tags:

views:

343

answers:

5

I have taken a liking to jQuery/Javascript's way of extending functionality via closures. Is it possible to do something similar in PHP 5.3?

class Foo
{
    public $bar;
}

$foo = new Foo;
$foo->bar = function($baz) { echo strtoupper($baz); };
$foo->bar('lorem ipsum dolor sit amet');
// LOREM IPSUM DOLOR SIT AMET

[edit] mixed up 'it' and 'is' in my question. heh.

UPDATE

I downloaded 5.3a3 and it does work!

class Foo
{
    protected $bar;
    public $baz;

    public function __construct($closure)
    {
        $this->bar = $closure;
    }

    public function __call($method, $args)
    {
        $closure = $this->$method;
        call_user_func_array($closure, $args);
    }

}

$foo = new Foo(function($name) { echo "Hello, $name!\n"; });
$foo->bar('Mon'); 
// Hello, Mon!
$foo->baz = function($s) { echo strtoupper($s); };
$foo->baz('the quick brown fox jumps over the lazy dog');
// THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG
+1  A: 

PHP can via create_function(). The syntax is cruciating though rendering it almost completely useless.

It seems PHP5.3+ is getting/has support for closures and lambda's!

However also from the RFC

Lambda functions / closures are not a way of dynamically extending classes by additional methods at runtime. There are several other possibilities to do this, including the already present _ _call semantic.

Martijn Laarman
I guess that quote answers my question. I wonder if the 'several other possibilities' include a way that could leverage the closure syntax somehow...
monzee
+1  A: 

Proper anonymous functions / closures are only available in PHP 5.3, which will not be widely available for a while - some useful information. I don't think it is really possible to do what you want.

You can use create_function() to an extent:

$func = create_function('$baz', ' echo strtoupper($baz); ');
$func("hello");

However the following does not work as you would expect:

$foo = new Foo;
$foo->bar = create_function('$baz', ' echo strtoupper($baz); ');
$foo->bar('lorem ipsum dolor sit amet');

You would need to do instead:

call_user_func($foo->bar, 'lorem ipsum dolor sit amet');

Also you can't use $this inside the created function because its scope would not be the same as a method.

Edit

I accidentally duplicated some of Martijn's answer as he edited his while I was writing mine. I will leave my answer intact for the sake of completeness

Another edit

You could maybe also do something like

class Foo
{
    public $bar;

    protected $anonFuncs = array();

    public function create_method($name, $params, $code) {
        if (strlen($params)) $params .= ',';
        $params .= '$self';
        $this->anonFuncs[$name] = create_function($params, $code);
    }

    public function __call($name, $args) {
        if (!isset($this->anonFuncs[$name])) {
            trigger_error("No method $name", E_USER_ERROR);
        }

        $args[] = $this;

        return call_user_func_array($this->anonFuncs[$name], $args);
    }
}

$foo = new Foo;
$foo->create_method('bar', '$baz', ' echo strtoupper($baz) . get_class($self); ');
$foo->bar('lorem ipsum dolor sit amet');
Tom Haigh
just beware, don't use it in a loop, as it will eat memory. functions are not freed once created.
Kent Fredric
Putting code inside quotes is... ugh. I was hoping the closures would let us do away with that.
monzee
yeah agreed. the closures in 5.3 will let you do away with it.
Tom Haigh
+3  A: 

PHP 5.3 will allow the creation of lambda functions and closures which would allow you to implement your code example. From the PHP RFC:

function & (parameters) use (lexical vars) { body }

The function is returned as reference and can be passed as a callback. For example, creating a function:

$lambda = function () { echo "Hello World!\n"; };

Passing a lambda function as a callback:

function replace_spaces ($text) {
    $replacement = function ($matches) {
        return str_replace ($matches[1], ' ', ' ').' ';
    };
    return preg_replace_callback ('/( +) /', $replacement, $text);
}

Closures also allow you to import variables from the local scope:

$map = function ($text) use ($search, $replacement) {
    if (strpos ($text, $search) > 50) {
       return str_replace ($search, $replacement, $text);
    } else {
        return $text;
    }
};

Where $search and $replacement are both declared outside the closure, and $text is the function parameter.

Eran Galperin
Carefull you don't define the "world!" namespace. </snide comment>
Kent Fredric
Yeah, I've read that closures will be coming in PHP 5.3, I'm just wondering if we're allowed to assign it to a class member.
monzee
Closures and lambdas are references to function (function assignment). You can assign it to anything you want.
Eran Galperin
+1  A: 

You can take advantage of __call method to re-route non-existent method calls. Add a registry to your class so you can add/remove methods on the fly. Calling convention for external function is that the first parameter is the object the function has been bound to.

clas Foo {

    private $registry;

    public function __call($alias, $args) {
        if (array_key_exists($alias, $this->registry)) {
            // prepend list of parameters with $this object
            array_unshift($args, $this);
            return call_user_func_array($this->registry[$alias], $args)
        }
    }

    public function register($method, $alias = null) {
        $alias or $alias = $method; // use original method name if no alias
        $this->registry[$alias] = $method;
    }

    public function unregister($alias) {
        unset($this->registry[$alias]);
    }

}

Imagine function clone_object that returns array of cloned objects. First parameter is object to be cloned, and the second is number of clones.

function clone_object($foo, $n) {
    $a = array();
    while ($n--) {
        $a[] = clone $foo;
    }
    return $a;
}

Now, this way you can inject method clone_object into class Foo and give it an alias name bar:

$f = new Foo();
$f->register('clone_object', 'bar');
$f->bar(5); // this will return array of 5 cloned objects

In the last line calling method bar will cause redirection to function clone_object. Mind that calling convention will insert parameter $this into parameters list, so (5) will be changed to ($this, 5).

The caveat is that external functions can't work on private/protected properties and methods.

Just a final word, if you can think of a solution that does not change objects on the fly, you should probably use it. The above is a black wizardry and should be used with extreme caution.

Michał Rudnicki
Thanks, that's an interesting snippet.
monzee
A: 

Using the create function you could do something like:

$Dynamic->lambda = create_function('$var', 'return $var." world."; ');
$classic = $Dynamic->lambda("hello"); // classic would contain "hello world."

The class code would contain something like:

public function __set($methodname, $function)
{
    $this->methods[$methodname] = $function;
}

public function __get($methodname)
{
    return $this->methods[$methodname];
}

public function __call($method, $args)
{
   $funct = $this->{$method};
   //$args[] = $this; --to add the current object as a parameter option
   return call_user_func_array($funct, $args);
}

Note: that you will not be able to use the object scope ($this variable) in your create function, as basically this is just using a object interface to create_function which is in the global namespace. However you can easily append an instance of the object to the parameter list and pull it into the class, but then again you would only have access to the public methods of the class pulled in.

null