Last Updated: May 31, 2021
·
24.7K
· lastguest

Wrapping a PHP class for easy method chaining

Method chaining is a cool and useful featured expecially for procedural sequential API.

What make possible chaining is returning at every method call the called object instance.

The Chainable class is a simple wrapper for making proxy call to class methods and returning the instance reference permitting method chaining.

For instance, consider the Test class which has three methods :

class Test {
    function foo(){ echo "FOO"; }
    function baz(){ return "BAZ"; }
    function bar(){ echo "BAR"; }
}

and this example use :

$test = new Test();

$test->foo();
$value = $test->baz();
$test->bar();

echo "\n",'The value was : ',$value;

/* Output:
    FOOBAR
    The value was : BAZ
*/

Consider now the opportunity to chain methods, this is what we want to achieve :

$test->foo()->bar();

For doing so, without modify the Test class, we must wrap it in another proxy class and redirect all calls to the wrapped instance.

class Chainable {
    private $instance = null;
    public function __construct(){
        $pars = func_get_args();
        $this->instance = is_object($obj=array_shift($pars))?$obj:new $obj($pars);
    }

    public function __call($name,$pars){
        call_user_func_array([$this->instance,$name],$pars);
        return $this;
    }

}

Now we can wrap Test with Chainable and chain methods calls :

$test = new Chainable('Test');

$test->foo()->baz()->bar();

/* Output:
    FOOBAR
*/

Nice! But wait... baz() returns a value... but our proxy is losing it.
We can fix that capturing returned method values in an internal cache and provinding a chainable method for retrieving them :

private $_returns = [];

 // Retrieve the returned value from last
 // chained method

public function _get_return(&$var){
    $var = count($this->_returns)?
              array_pop($this->_returns):null;
    return $this;
}

// Clear the returned value cache
public function _reset(){
    $this->_returns = [];
    return $this;
}

// Redefine call proxy for saving returned values
public function __call($name,$pars){
    ($r=call_user_func_array([$this->instance,$name],$pars))?$this->_returns[]=$r:null;
    return $this;
}

Now, we can retrieve the saved value during the calls chain :

$test = new Chainable('Test');
$test
    ->foo()
    ->baz()->_get_return($the_value)
    ->bar();

echo "\n\n",'The value was : ',$the_value;
/* Output:
    FOOBAR
    The value was : BAZ
*/

And we're done.

You can find the complete class and this example here :
https://gist.github.com/lastguest/4732993

12 Responses
Add your response

great tip!, thanks for sharing :)

over 1 year ago ·

nice one! although I feel like a little explanation would help more. like when and why chaining methods are important or may be a little performance issues/advantages of using it. But I guess that would make it more of a tutorial than a protip lol.

Btw, I've seen laravel's eloquent ORM using chain methods in their dynamic query builder. it's really expressive and handy!

over 1 year ago ·

@bdgeeksfoysal Yeah knowing why would be very useful

over 1 year ago ·

@obayesshelton , @bdgeeksfoysal
Here is an example for you :
https://coderwall.com/p/_ss6sa
^__^

over 1 year ago ·

Thank you Stefano, this is very useful.

over 1 year ago ·

Why do you have an array of return values?

Note that with the above code you don't need to have the getreturn() directly after the call that returns the value, but you can get the return values later in the chain:
$test = new Chainable('Test');
$test
->baz() // call 1
->baz() // call 2
->foo() ->getreturn($returnvalueofcall2)
->foo() ->getreturn($returnvalueofcall1);

over 1 year ago ·

Yes, the return queue is a LIFO, and you can retrieve it even at the end of the chain. However It's useful using the get_return method after the call result you want to retrieve, just in case you need the return value for passing it as parameter for the next chained methods.

Why do you have an array of return values?

Simply for convenience.
If you need, you can also implement a clearresults() method for purging the results queue, freeing some memory.

However you are free to change it to a single variable, storing results and replacing previous value.

over 1 year ago ·

I don't think this is the best way to be explaining method chaining to someone unfamiliar. Creating a factory for classes just to add method chaining isn't really best practice. Consider if a class' methods all return a specific value. Method chaining would be useless. If you want to take advantage of method chaining, modify the classes as you see fit, using return $this; where appropriate. There are lots of great examples out there, like Eloquent as mentioned, amongst others (most ORMs are a good example, really).

You also have a weird coding style that's difficult to follow. Multiple lines for method parameters and ternary ops, non-descriptive variables, etc. That's fine for your own applications, but if you're trying to teach people, keep it as simple as possible.

over 1 year ago ·

@cryode

If you want to take advantage of method chaining, modify the classes as you see fit, using return $this; where appropriate.

You can't (or mustn't) always change the direct source of the target class. This is a tip for a wrapper class, not a lesson on how to implement a Named Parameter Idom Design Pattern.

That's fine for your own applications, but if you're trying to teach people, keep it as simple as possible.

Again, it's a tip, not a lesson. If you have a simpler, descriptive, version of a wrapper class for method chaining, please post it here, I'll be glad to learn from it. :)

over 1 year ago ·

I wouldn't suggest someone use a wrapper for the sole purpose of adding method chaining in the first place, so I don't have a better solution.

And regardless of the length or depth of this tip, you're still attempting to teach people something. Your code style makes that more difficult. Just something you may want to consider in the future.

over 1 year ago ·

Bad news is a lack of code-completion.

over 1 year ago ·

I agree with cryode. Code which is shared with other people should be easier readable. Who is interested in dissecting shorthand code? With such a coding style you diminish the value of a generally quite interesting post.

over 1 year ago ·