6

I have a class Logger which, among other things has a method Log.
As Log is the most common use of the Logger instance, I have wired __invoke to call Log

Another class, "Site" contains a member "Log", an instance of Logger.

Why would this work:

$Log = $this->Log;  
$Log("Message");  

But not this:

$this->Log("Message");

The former fails with "PHP Fatal error: Call to undefined method Site::Log()"
Is this a limitation of the callable object implementation, or am I misunderstanding something?

Matthew Scharley
  • 127,823
  • 52
  • 194
  • 222
James Maroney
  • 3,136
  • 3
  • 24
  • 27

5 Answers5

7

Unfortunately, this is (still) a limitation of PHP, but it makes sense when you think about it, as a class can contain properties and methods that share names. For example:

<?php
class Test {
    public $log;

    public function __construct() {
        $this->log = function() {
            echo 'In Test::log property';
        };
    }

    public function log() {
        echo 'In Test::log() method';
    }
}

$test = new Test;
$test->log(); // In Test::log() method
call_user_func($test->log); // In Test::log property
?>

If PHP were to allow the syntax you desire, which function would be invoked? Unfortunately, that only leaves us with call_user_func[_array]() (or copying $this->log to another variable and invoking that).

However, it would be nice if the following syntax was acceptable:

<?php
{$test->log}();
?>

But alas, it is not.

rintaun
  • 697
  • 8
  • 14
  • This is a weird quirk indeed. Something like ($test->log)() seems mostly a limitation of the parser, not of the language itself. – Erik van Velzen Sep 05 '14 at 16:23
1

This may work:

${this->Log}("Message");

But perhaps it's just easier and better to use the full call? There doesn't seem to be a way to get what you want to work on the one line.

The error in your question indicates it is looking for a function defined on the class which doesn't exist. An invokable object isn't a function, and it seems it can't be treated as one in this case.

Matthew Scharley
  • 127,823
  • 52
  • 194
  • 222
1

Same reasons you can't do this:

$value = $this->getArray()["key"];

or even this

$value = getArray()["key"];

Because PHP syntax doesn't do short hand very well.

Kendall Hopkins
  • 43,213
  • 17
  • 66
  • 89
  • Mostly because of the loose typing. There's no way of actually infering what you might mean to do, so it defaults to erroring early. – Matthew Scharley Nov 01 '09 at 02:21
  • I've written an odd preparser which allows to write such expressions: http://code.google.com/p/php-preparser/ :) (can I "advertise" my open-source solutions here?) – Valentin Golev Nov 01 '09 at 02:23
1

as of php7.4 the following code works for me

($this->Log)("Message");
0

Thank you for your question and for all the answers! I'll share my experience in case it comes useful for someone on this subject.

I love defining a debug function in such a way that the program, when debug flag is on, would describe to me what it's doing, and I add a lot of the logging when I write the code, as this is the time when I know best which cases can be there. Then when debug flag is off, there is almost no overhead cost except for boolean checks, since the function is called (and the parameters are evaluated) only in case debug is on.

I used it in other languages in a different way, and in PHP at first I wrote it like this:

const DEBUG_LOGGING = true;
$logdbg = DEBUG_LOGGING ?
    function(...$args) {
        foreach ($args as $arg) {
            echo var_export($arg, true), PHP_EOL;
        }
    } :
    null;

but then it would be in the global scope:

//this would work:
$logdbg && $logdbg($var1, $var2);


function test() {
  //some code
  
  //this wouldn't work:
  $logdbg && $logdbg($var3, $var4);
  
  //it would have to be:
  global $logdbg; $logdbg && $logdbg($var3, $var4);
  
  //other code
}

and I didn't want to add global variables, which couldn't be put under a namespace. So after checking what can be in a namespace, I defined it inside a class:

const DEBUG_LOGGING = true;
class Dbg {
    public static $log = null;
    public static function init() {
        if (DEBUG_LOGGING) {
            self::$log = function(...$args) {
                foreach ($args as $arg) {
                    echo var_export($arg, true), PHP_EOL;
                }
            };
        }
    }
}
Dbg::init();

Dbg::$log && Dbg::$log('outside function', $var1);

function test() {
  //some code
  
    Dbg::$log && Dbg::$log('inside function', $var2, $ar3);
  
  //other code
}
test();

but it gave me the same "uninitialized" warning this thread speaks about, and didn't work!

Thanks to this thread and Norbert Wagner's recommendation, I tried with parentheses and it worked! I didn't need to add it on the boolean check, only on the call, and now the code looks like this and works:

//the only difference with the previous snippet
Dbg::$log && (Dbg::$log)('outside function', $var1);

Dbg::$log && (Dbg::$log)('inside function', $var2, $ar3);
Olga Farber
  • 306
  • 2
  • 7