Magical PHP: __call()

Submitted by Larry on 21 August 2007 - 11:35pm

PHP is, conceptually, a very traditional language. By that I mean that it is steeped in the C/C++/Java mindset of how a program is put together, rather than the LISP/Javascript/Ruby/Python mindset. That's not a bad thing, necessarily, but it does mean that a lot of the cool dynamic language capabilities in those languages isn't really available to PHP developers. I freely admit to being jealous of functions as first-class objects, for instance.

PHP 5, however does include lots of "magic" capabilities, some in the object model directly and some via SPL Interfaces, that, if used properly, can make up for a lot of that lack of dynamic capaibility. A favorite of mine is the ability to add methods to a class at runtime.

What? You didn't know PHP can do that? Well, that's because it can't. However, we can simulate it pretty closely if we're careful. Let's see how.

Calling magic

PHP 5 includes a number of magic methods that a class can implement. Each magic method begins with __, and gets called automatically by PHP under certain conditions. Technically some are called overloading methods rather than magic methods, but they all effectively work by the same magic blue smoke.

Today we're just going to look at one of them: __call(). If a class implements __call(), then if an object of that class is called with a method that doesn't exist __call() is called instead. To wit:

<?php
class Caller {
  private
$x = array('a', 'b', 'c');

  public function

__call($method, $args) {
    print
"Method $method called:\n";
   
var_dump($args);
    return
$this->x;
  }
}

$foo = new Caller();
$a = $foo->test(1, 2, 3);
var_dump($a);
?>

Other PHP components like SOAP use __call() to support dynamic methods based on a remote object. How does that help us add methods to a class at runtime, though?

Registering magic

We could define all of the methods within the class and then have the __call() method route to them, but then we have to define the methods within the class, which defeats the purpose. But if we define them outside of the class, how do we treat them like a method?

First, we have to register the pseudo-method with the class:

<?php
class Dynamic {
  protected
$methods = array();

  public static function

registerMethod($method) {
   
self::$methods[] = $method;
  }
}

function

test() {
  print
"Hello World" . PHP_EOL;
}

Dynamic::registerMethod('test');
?>

And then it becomes obvious how to call it.

<?php
class Dynamic {
  static protected
$methods = array();

  public static function

registerMethod($method) {
   
self::$methods[] = $method;
  }

  private function

__call($method, $args) {
    if (
in_array($method, self::$methods)) {
      return
call_user_func_array($method, $args);
    }
  }
}

function

test() {
  print
"Hello World" . PHP_EOL;
}

Dynamic::registerMethod('test');
$d = new Dynamic();
$d->test();
?>

Neat. But really, all we've done is create a level of indirection to a function. It's not a method. test() doesn't even know what its object is. That's trivial to fix, though:

<?php
class Dynamic {
 
//...
 
public $message = 'Hello World';

  private function

__call($method, $args) {
    if (
in_array($method, self::$methods)) {
     
array_unshift($args, $this);
      return
call_user_func_array($method, $args);
    }
  }
 
//...
}

function

test($obj) {
  print
$obj->message . PHP_EOL;
}

Dynamic::registerMethod('test');
$d = new Dynamic();
$d->test();
?>

OK, so now we use $obj instead of $this in the pseudo-method, but otherwise we're golden. We can take any function that takes an object as its first parameter and assign it to a class, and poof, we now have a new method!

Or do we?

Private magic

We're still missing one key property of methods. A method of an object has access to that object's private and protected properties. In our case, the psuedo-method is still treating the object as a foreign object (which it is), and so doesn't have access to private methods or properties. What's more, if there was a way for our function to get at private properties of $obj, it would be a bug in the PHP engine. It's not supposed to have access to those properties. That's why they're private!

Although at this point it seems hopeless, we're still not done. A function outside of an object cannot access private properties of that object, but an object can pass any property it wants to another method or function. Passing the entire object's internal properties as parameters to the psueudo-method would be very sloppy and very brittle, however. Instead, let's use a facade object. That facade object will hold no data, but just references to data. References in memory ignore public/private status, because the variable (reference) is protected, not the data itself. We let the object being referenced set everything up, because it has access to its own properties. To wit:

<?php
class Dynamic {
  static protected
$methods = array();
  private
$message = 'Hello World';

  public static function

registerMethod($method) {
   
self::$methods[] = $method;
  }

  private function

__call($method, $args) {
    if (
in_array($method, self::$methods)) {
     
$obj = new DynamicFacade($this);
     
$fields = array();
      foreach (
array_keys(get_class_vars(__CLASS__)) as $field) {
        if (
$field != 'methods') {
         
$fields[$field] = &$this->$field;
        }
      }
     
$obj->registerFields($fields);
     
array_unshift($args, $obj);
      return
call_user_func_array($method, $args);
    }
  }
}

class

DynamicFacade {

  private

$object = NULL;
  private
$fields = array();
  private
$arrays = array();

  function

__construct($obj) {
   
$this->object = $obj;
  }

  private function

__get($var) {
    if (
in_array($var, array_keys($this->fields))) {
      return
$this->fields[$var];
    }
    else {
      return
$this->object->$var;
    }
  }

  private function

__set($var, $val) {
    if (
in_array($var, array_keys($this->fields))) {
     
$this->fields[$var] = $val;
    }
    else {
     
$this->$object->$var = $val;
    }
  }

  private function

__isset($var) {
    if (
in_array($var, array_keys($this->fields))) {
      return
TRUE;
    }
    return isset(
$this->object->$var);
  }

  private function

__unset($var) {
    unset(
$this->object->$var);
    unset(
$this->fields[$var]);
  }

  public function

registerFields(&$fields) {
   
$this->fields = $fields;
  }

  function

__call($method, $args) {
    return
call_user_func_array(array($this->object, $method), $args);
  }
}

function

test($obj) {
  print
$obj->message . PHP_EOL;
}

Dynamic::registerMethod('test');
$d = new Dynamic();
$d->test();
?>

Here, we now build a facade object within the __call() method. That call object has a reference to our dynamic class object. Then we create an array that contains references to our dynamic object's properties. Those references ignore public/private concerns. Then we pass that array, by reference, to the facade object. The facade object then has an array whose values reference directly to the properties of the dynamic object. We then use two other overloading methods, __get() and __set(), to route requests against the facade object back to the original object's properties. Try it!

A more complex implementation could even make pseudo-methods conditional, say, on the type of an object. It's not multiple inheritance... but it tastes almost like it.

Limitations

There's still one problem with this mechanism. Because of the way arrays work, we can't modify private arrays. They don't resolve properly. I still haven't found a solution to that issue, sadly. If anyone else has a solution, I'd love to hear it.

There's also the performance question. Any level of indirection adds CPU cycles, sometimes a lot. How does creating a facade object for each pseudo-method request stack up? It turns out that creating the facade object has only a negligible impact on performance. PHP 5's engine is really quite good at creating objects. Unfortunately, it's not so good at calling dynamic methods. Just triggering __call() itself is, in fact, quite slow. In my benchmarks, it came out as 12 times slower than a normal method call. To be fair, we're talking about 12 times a fraction of a millisecond, and I had to crank the number of iterations up to a million in order to test it, but it's still disappointing.

Update: Looks like it's not __call() that's killing performance. It's call_user_func_array(). Yay for benchmarks. :-)

Suggestions for improvement are always welcome. In the mean time, let me know if you were able to leverage this capability in a production system! I always like knowing my tutorials prove useful somewhere.

Happy Coding!

Anonymous (not verified)

13 March 2008 - 8:36am

Strikes me as a bad idea to have 'class methods' outside the class. It's like OO encapsulation gone horribly wrong... *cringe*

nocash (not verified)

8 July 2008 - 12:33am

An interesting read.

Just a note: according to the PHP manual, "All overloading methods must be defined as public."

An comparable solution I used in a project, but instead of the solution above enterily OO:

class ProductPart {

        protected $data;
        protected $plugins = array();
        function __construct($data){

                $this->data = $data;

        }
        public function register(ProductPlugin $plugin){

                if(!in_array($plugin, $this->plugins)){

                        $this->plugins[$plugin->toString()] = $plugin;
                } else {
                        throw new Exception('Function allready defined');
                }
        }
        public function unregister(ProductPlugin $plugin){
                if(isset($this->plugins[$plugin->toString()])){
                        unset($this->plugins[$plugin->toString()]);
                } else {
                        throw new Exception('No such function');
                }

        }

        protected function __call($method, $args) {
              if(isset($this->plugins[$method])){
                array_unshift(&$args, $this->data);
                array_unshift(&$args, $this);
                return $this->plugins[$method]->run($args[0], $args[1], $args[2]);

              } else {
                throw new Exception('No such function');
              }
        }
}

I simplified the class somewhat for clearity.

With this class, you can dynamicly add and remove classes by calling register or unregister. Register will store the object in an associative array by calling toString (as defined by ProductPlugin) and saving the method under the returned string in the array. (In this case the name of the method the class adds.)

When a method is called, which isn't standard in the object, _call will lookup the called method in the array. If found, __call run the method of the plugin with the provided arguments. I restricted the user provided argument to 1, because I want to force the user to use associative arrays.

Because I chose an array to store my classes, removing a function becomes a lot simpler. However the unregister function isn't optimal, I better pass a string instead of a plugin object. I didn't test it yet on performance.

The ProductPlugin class:

abstract class ProductPlugin {
        protected $name = null;
        abstract public function run($obj, $data, $args);
        public function __construct($data = null) {

                if($this->name === null){
                        throw new Exception('Name must be defined');
                }
                $this->init($data);
        }
        protected function init($data){

        }
        public function toString(){
                return $this->name;
        }
}

As a side note. It can make things pretty obscure. Why?

$product = new ProductPart();
...
$plugin = new ComplexOperationPlugin();
$product->register($plugin);
...
...
$product->complexOperation();
...
...
$plugin->doSomethingWithProduct();

ComplexOperationPlugin stores the reference of ProductPart object -not sure about this-, when run is called. If doSomethingWithProduct is called, the plugin can handle on the object, which I think is a bad thing. Because it is not clear the method does something with the ProductPart object.