My implementation of PHP Chainable March 2, 2010
I needed a way to use method chainability on objects without having to modify the actual class (e.g. extending an Chainable class or implementing one) and without having to “return $this” on every method.
I came up with, what I believe to be, an elegant simple solution – a generic approach that can be used on any class/object, which makes chaining possible on 3rd-party classes and enables you to return values from those methods.
For the lazy ones among us, it also allows to have the benefits of method chaining without having to think about it while coding. It just works.
So, first of all, this is the Chainable class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | class Chainable { private $obj; private $result; public function __construct($obj){ $this->obj = $obj; } public function __call($method, $args) { if (substr($method, 0, 1) == '_') { array_unshift($args, $this->result); $method = substr($method, 1); } $this->result = call_user_func_array(array($this->obj, $method), $args); return $this; } // __set & __get were added later on and mentiond at the very end of the post public function __set($key, $value){$this->obj->$key = $value; return $this;} public function __get($key){return $this->obj->$key;} public function result(){ return $this->result; } // new Chainable($obj)->foo() isn't possible, Chainable::get($obj)->foo() is public static function get($obj){ $chainable = new Chainable($obj); return $chainable; } } |
Basically, you use instances of Chainable instead of working directly with the actual class. You get a new instance by using new Chainable($object), or the more convient way which allows to call methods of it right away – Chainable::get($object).
After getting a Chainable object that’s connected to your object, you simply use $chainableObj->method1()->method2().
If you want to pass the result of the last method call to the next method, you prefix the method that should receive the result with a “_”, and than the first argument passed to it would be the last return value.
Also, you have a result() method for getting the last result from “outside”. this does create problems when you actually have a result() method on the object you’re passing to Chainable, and I will probably change it to something less likely to be used.
Lets take an example class, Foo:
1 2 3 4 5 6 7 8 9 | class Foo { public function bar($str){ return 'bar::' . $str; } public function taz($passed, $str) { return 'taz::' . $str . ' -- passed::' . $passed; } } |
Than, to use the Chainable interface you:
$foo = new Foo; echo Chainable::get($foo)->bar('shesek')->_taz('test')->result();
Which prints “taz::test — passed::bar::shesek”. note the “_taz” which allows taz() to get the return value of bar() as the first argument, $passed.
Another approach could be adding a “chain” method inside the Foo class (probably much easier to make a base class that has this and extend):
public function chain(){ return Chainable::get($this); }
Which enables you to use $foo->chain()->bar(‘shesek’)->_taz(‘test’)->result();. It makes the code prettier, with the price of having to modify the class itself – but a very minor modification.
A drawback of this concept is that it prevents access to private/protected methods, and I will often use this from inside the object (Chainable::get($this)->foo()->bar()) .
To solve that, I’m thinking about making a generic public method that’ll serve as a kind of “proxy” to private methods – but it does kinda make private/protected methods purposeless…
I haven’t used it much yet (got the idea around 20 minutes ago and wrote the Chainable class and testcase while writing this post), but I do think I’ll find this to be quite effective and useful.
This code is released under the SMWTFPL license, so if any of you find this useful feel free to use it.
If you have any remarks or thoughts, I’ll be happy to hear about it!
Update: Added __set & __GET
__set – Now its possible to set variables in the object while chaining (example with PHPMailer):
$mail=new PHPMailer(); Chainable::get($mail)->__set('Subject','Hello there')->AddReplyTo('my@email.info')->SetFrom('my@email.info')->AddAddress('your@email.info')->msgHTML('<h1>Hey!</h1>')->send()->result()
This is equal to $mail->Subject=’…’;. This can be used as a magic setter too, but doesn’t make much sense.
__get allows you to read public variables of the object. it only makes sense if you use that last (that HTTP class doesn’t exists, just for showing the concept):
$http = new HTTP; echo Chainable::get($http)->__set('url', 'http://www.google.com/')->userAgent('Firefox')->request()->html;
The last -> html is equal to $http->html
Leave a Reply