Monday, August 31. 2009
Extending objects with new methods at runtime
While the title of this blog entry might sound rather scary to straight OO evangelists it might attract other developers - e.g. those that played around with runkit or had a look into the Ruby world where the language supports adding new methods to a class or just to an instance of a class at runtime (see singleton methods, and this blog article; for those not familiar with Ruby: Fixnum is a class already defined by Ruby core.)
With the advent of PHP 5.3 adding new methods to an instance of a class at runtime becomes possible with PHP as well, using anonymous functions and a little bit of __call() magic. First, let's define a new class (boring old foo, bar, baz example for lack of fantasy):
class Foo
{
public function bar()
{
echo "This is Foo::bar()\n";
}
}
As you surely already know you can add public properties to an instance at any time:
$foo = new Foo();
$foo->baz = 'Hello World.'`;
We can use this to store an anonymous function:
$foo = new Foo();
$foo->baz = function () { echo "This is Foo::\$baz()\n"; };
Unfortunately, we are not able to call this as a method:
$foo->baz();
This will blow up with a fatal error saying "Call to undefined method Foo::baz()". Of course we could do
$func = $foo->baz;
$func();
but that is not what we wanted to achieve. Let's add some __call() magic to our Foo class:
class Foo
{
// method bar() omitted
public function __call($method, $args)
{
if (isset($this->$method) === true) {
$func = $this->$method;
$func();
}
}
}
Now we can safely call $foo->baz() with the desired result. However, compared to Ruby you can not redefine existing methods. Therefore,
$foo->bar = function () { echo "This is Foo::\$bar()\n"; };
$foo->bar();
will still call the method Foo::bar() defined earlier and ignore the redefinition due to the nature of how __call() works. What can you do with it? If you use duck typing this might be useful as it reduces the amount of code required for the Adapter design pattern as you just extend the instance you want to use instead of creating a separate adapter class. The result is the same, both a full fledged adapter class and the closure can only access the public properties and methods of the instance to adapt. Problem is, the class to adapt most likely does not have the required __call() implementation. Another use case could be a simplified version of the extension methods mechanism where you want to add a method locally without the need to have it available globally in the application. If you have another good idea of what this can be used for please feel free to comment - just wanted to write my thoughts down. ![]() ![]() Trackbacks
Trackback specific URI for this entry
No Trackbacks
![]() Comments
Display comments as
(Linear | Threaded)
Probably we should first try to encapsulate behaviours in a single class rather than spreading them around in this way. Simple solutions - if they exist - are always better than clever ones.
That said, there are times when I think you do need something like this. Specifically, if you need to decorate objects with several discrete behaviours and you need to add these in different combinations. For example, in a test case you might have a bunch of new assert~ methods tailored to a specific domain which you want to re-use in other tests. Or some fixture classes. That led me to something similar: http://aperiplus.sourceforge.net/method-injection.php.
> as you just extend the instance you
> want to use instead of creating a > separate adapter class That in my view only encourages lazyness and the (continued?) use of bad programming practices. Why not simply use an Adapter? It's not that much more overhead and you still follow those important practices. Besides using the approach you have described is a hack anyway... yuk ![]()
To be honest im not convinced either.
Im not saying that its a bad thing but it does not sound 'safe'. Will it not increase dependencies a lot? Will it not make your objects change randomly as they get passed around the app? Who adds methods? when? why? Is the instance i have already have the method i want to call? Will the design and readability benefit from that saving of a new class file or a few lines of code? Could you use aggregation and take instance implementing particular interface in constructor or whenever? then just add wrapper methods. This makes interface of the class constant and behavior can still change. Maybe if you could please provide real world useful usecase it would convince me its a good thing. So far i would rather stay away from 'hack' as i dont mind writing additional bits of code : -)
Thanks for your responses so far. I'm not convinced that it is a good thing to do anyway. I just stumbled about the fact that this is supported by the Ruby language natively and wondered how this could be achieved with PHP and what it can be used for.
In general I would not use something like this. However, each rule has its exceptions, so in some cases it may be a valid usage. Thinking about our enum implementation in Stubbles it might be useful there but not to the extend to add new methods from outside the enum.
Two notes:
a) $foo->baz() should eventually be possible (once the resolution order has been fixed; there was no time to finish that for 5.3) b) You cannot access $this inside the function; that will be possible in a future release too
That's interesting, because a) would allow to add methods on instances of 3rd party classes. Sometimes this is useful because a full fledged adapter would be overkill, but one should use it with care.
Yeah extend everything anywere is good so nobody can see where it´s extended and you could hide small extra behavior somewhere where other people might not see it at once.
Cool post! The thought that this was possible popped into my head a few weeks ago and I hadn't had a chance to try it yet. Thanks for checking it out!
As an objective-C coder I have been waiting for such functionality - it exists as a feature in obj-c called "Categories". It's also being discussed for PHP in the grafts & traits RFCs. It definitely does break black-box and encapsulation, but it does add a lot of flexibility. I have not yet decided myself if this is 100% a good thing. It has many downsides as discussed by the other commenters including increasing opacity and affects coupling. However most of us have used the power of such systems with javascript tools like Prototype. Since javascript has a prototypal object architecture this has always been possible is Javascript as well as ruby.
A very interesting possibility - but i am still not sure if this if really clean.
But could be helpful in much cases, especially when you just need to add some little functionality without really extending the class. ThX for the great post!
Interesting take on attaching methods at runtime. You can actually do this pre-5.3 (the Recess Framework has had AttachedMethod's since v0.1 released last year) with a little more indirection (map to another class' method instead of a Closure's __call). Recess' attached methods can be reflected over, too. This is how we introduce relationship methods in our ORM.
You could expand the usefulness of your __call method by passing the args along to your dynamically dispatched function. Using call_user_func_array instead of invoking the lambda directly opens up a lot of new options (i.e.: $foo->baz = 'aPlainOldFunction'; or $foo->baz = array('AnotherClass','AStaticMethod');) You'll also likely want to unshift a reference to $this onto the front of your arguments array so that the callable has a reference back to the method. (Ex: http://github.com/recess/recess/blob/95c71a8f81bb7a2c3977c57196fa5fabda8874b0/recess/recess/lang/Object.class.php#L98 ) I actually just wrote a related post on callables in PHP 5.3: http://www.recessframework.org/page/php-callables-is-callable-call-user-func-array-reflection Looking forward to $object->lambda() being a first class scenario in future versions of PHP.
That was already possible before using create_function().
create_function uses eval() internally and as such is less performant and less "integrated" compared to the approach shown here. Essentially, eval is evil - avoid its use at all costs.
![]() ![]() ![]() |
![]() ![]() Calendar
![]() Archives![]() Categories![]() Quicksearch![]() ![]() Blog Administration![]() ![]() |