views:

36

answers:

3

Recently in a project I am working on, I needed to store callbacks in a static member array, like so:

class Example {
    private static $_callbacks = array(
        'foo'=>array('Example','do_foo'),
        'bar'=>array('Example','do_bar')
    );
    private static function do_foo() { }
    private static function do_bar() { }
}

To call them, I tried the obvious (maybe even naive) syntax (inside the Example class):

public static function do_callbacks() {
    self::$_callbacks['foo']();
    self::$_callbacks['bar']();
}

To my surprise, this did not work, resulting in a notice that I was accessing an undefined variable, and a fatal error stating that self::$_callbacks['foo'] needed to be callable.

Then, I tried call_user_func:

public static function do_callbacks() {
    call_user_func(self::$_callbacks['foo']);
    call_user_func(self::$_callbacks['bar']);
}

And it worked!

My question is:

Why do I need to use call_user_func as an intermediary, and not directly call them?

+2  A: 

Because in PHP functions are not really data type as they are in more functional languages (which is why they are specified in a relatively awkward array syntax - even create_function() only returns the generated name of the function, not a function pointer). So, call_user_func and it's brethren are workarounds (hacks?) to allow some form of function/callback passing ability.

Brenton Alker
That's one reason. The other is that the script isn't parsed the way he thinks it is.
Artefacto
+2  A: 

You can't call callbacks by appending (). That only works in PHP 5.3 with lambda functions and objects that implement the __invoke magic (see also the internal get_closure object handler).

First, despite what you say, this doesn't work:

<?php
class Example {
    private static $_callbacks;
    private static function do_foo() { echo "foo"; }
    private static function do_bar() { echo "bar"; }

    public static function do_callbacks() {
        self::$_callbacks['foo'] = array('Example','do_foo');
        self::$_callbacks['bar'] = array('Example','do_bar');

        self::$_callbacks['foo']();
        self::$_callbacks['bar']();
    }
}
Example::do_callbacks();

But it wouldn't even work if self::$_callbacks['foo'] was a lambda:

<?php
class Example {
    private static $_callbacks;

    public static function do_callbacks() {
        self::$_callbacks['foo'] = function () { echo "foo"; };

        self::$_callbacks['foo']();
    }
}

Example::do_callbacks();

The reason is the parser. The above compiles to:

Class Example:
Function do_callbacks:
(...)
number of ops:  16
compiled vars:  !0 = $_callbacks
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   6     0  >   EXT_NOP                                                  
   7     1      EXT_STMT                                                 
         2      ZEND_FETCH_CLASS                                         
         3      ZEND_DECLARE_LAMBDA_FUNCTION                             '%00%7Bclosure%7D%2Ftmp%2Fcp9aicow0xb7fcd09b'
         4      FETCH_W                      static member       $2      '_callbacks'
         5      ZEND_ASSIGN_DIM                                          $2, 'foo'
         6      ZEND_OP_DATA                                             ~3, $4
   9     7      EXT_STMT                                                 
         8      FETCH_DIM_R                                      $5      !0, 'foo'
         9      ZEND_FETCH_CLASS                                         
        10      ZEND_INIT_STATIC_METHOD_CALL                             $6, $5
        11      EXT_FCALL_BEGIN                                          
        12      DO_FCALL_BY_NAME                              0          
        13      EXT_FCALL_END                                            
  10    14      EXT_STMT                                                 
        15    > RETURN                                                   null

There's never a fetching of a static member (except for the assignment of the lambda). In fact, PHP compiles a variable $_callbacks, which turns out not to exist at runtime; hence your error. I concede this is, maybe not a bug, but at least a corner case of the parser. It evaluates the $_callbacks['foo'] part first and then attempts to call the static function whose name results from that evaluation.

In sum – stick to call_user_func or forward_static_call.

Artefacto
Oh wow, you're right: that doesn't work! That's really weird, because when I was originally trying to figure out why my original didn't work, I tried that and it did work. I'll pull that out of my question, then.
Austin Hyde
A: 

If you are using PHP 5.3, and the callbacks are always static functions of a class, then you can use this code:

public static function do_callbacks() {

  // Those can be defined in another function, or in the constructor.
  // self::$_callbacks['foo'] = array('Example','do_foo');
  // self::$_callbacks['bar'] = array('Example','do_bar');

  $class = self::$_callbacks['foo'][0];
  $method = self::$_callbacks['foo'][1];
  $class::$method();

  $class = self::$_callbacks['bar'][0];
  $method = self::$_callbacks['bar'][1];
}

In that way, you don't need to use call_user_func(). It makes sense to use this method if the callbacks are always of the same type (in this case, they were static methods of a class whose name was given as first item of an array). If you need to consider all the possible type of callbacks call_user_func() is able to handle, then it's better to use call_user_func(); PHP code cannot be faster than C code used to implement PHP extensions.

kiamlaluno
Yeah, I considered this, but I do need to support anything that `is_callable()`.
Austin Hyde