Transparent aggregation in PHP5

The other day a Ruby developer was extolling the virtues of Ruby on Rails to me, in particular the use of “acts_as_” to make a class magically include all the behaviour from another. It isn’t inheritance, the relationship isn’t “is a” but rather “has a” – it’s a sort of transparent aggregation with all the methods and properties of the aggregated object magically available through the containing object. At least that’s what it looks like, from my somewhat limited RoR experience.

Personally, I would rather achieve this functionality by using the Strategy Pattern – it requires more boiler-plate code but I think the end result is much clearer. If you’re reading this via o2 mobile broadband or another portable connection to the web, then prepare to miss your stop on the train, as my temptations to re-work the functionality issues began to take over just lower down the page.

Anyway, not to be outdone, I set about implementing RoR’s acts_as in PHP.   My implementation works by using a class called Aggregatable which allows all child classes to aggregate, or “act_as”, other standard classes.   I’ll start with an example with two simple classes; Binman and Postman which each do different jobs.

Here are the classes:

class Binman
{
    public function collectRubbish()
    {
        printf("Collecting rubbish\n");
    }
}
class Postman
{
    public function deliverLetter()
    {
        printf("Delivering a letter\n");
    }
}

We can use the classes like this:

$binman = new Binman();
$binman->collectRubbish();    // Prints "Collecting rubbish"

$postman = new Postman();
$postman->deliverLetter();    // Prints "Delivering a letter"

So far so good, but what if I wanted a CasualWorker class to be able to both collect rubbish and deliver letters?   I could of course copy the methods from the Binman class and the Postman class into the CasualWorker class, but this would be rather inflexible – suppose if we needed to change the Binman behaviour we would have to update both the Binman and CasualWorker classes.   Instead we make the CasualWorker class a child of the Aggregatable class and aggregate the Binman and Postman classes.   That might sound a bit complex so I’ll illustrate with an example.

Here’s the CasualWorker class:

class CasualWorker extends Aggregatable
{
    public function __construct()
    {
        $this->aggregate("Binman");
        $this->aggregate("Postman");
    }
}

The CasualWorker class can now do everything that the Binman and Postman classes can:

$casualWorker = new CasualWorker();
$casualWorker->collectRubbish();    // Prints "Collecting rubbish"
$casualWorker->deliverLetter();    // Prints "Delivering a letter"

Great!   It works!   But how?

The Aggregatable class contains an array of instances of all the classes it has been asked to aggregate.   It also makes use of PHP’s magic method, __call(), so that when client code invokes a method that is not defined by it or the child class it is extending, it loops through the objects it is aggregating to see if any of those define the method.   If an object is found that defines the requested method then it is run and the result returned – just as if the invoked class defined the method.

class Aggregatable
{
    /**
     * Store of aggregated objects
     *
     */
    private $aggregated = array();

    /**
     * Aggregates objects
     *
     * @param string Classname
     */
    protected function aggregate($class)
    {
        // Check an instance of this class has not already been aggregated
        if (array_key_exists($class, $this->aggregated))
        {
            throw new Exception(sprintf("Class already aggregated: %s", $class));
        }

        // Add a new instance of the class to the store
        $this->aggregated[$class] = new $class();
    }

    /**
     * Magic method - catch calls to undefined methods
     *
     * @param String method name
     * @param array Arguments
     */
    public function __call($method, $arguments)
    {
        // Loop through the aggregated objects
        foreach ($this->aggregated as $subject)
        {
            // Check each object to see if it defines the method
            if (method_exists($subject, (string) $method))
            {
                // Object defines the requested method, so call it and return the result
                return call_user_func_array(array($subject, (string) $method), $arguments);
            }
        }

        throw new Exception(sprintf("Method not found: %s", $method));
    }
}

What about properties?

I’ve kept the example above fairly simple but there’s no reason why it couldn’t be extended to cover properties using the other Magic Functions __set(), __get() and __unset().   In fact, in the example in the download (see below) I’ve implemented basic support for properties.

Conclusion

Like I’ve said before, I’m not a big fan of this sort of “magic”.   It seems to me to break the golden rule of object oriented programmed, as mentioned in the Gang Of Four book; “program to an interface, not an implementation”.   The interface the class provides is defined by the public properties and methods available, or even better, from the Interfaces it implements.   By adding this sort of run-time dynamism, the client code can never be sure what the class can do, instanceof won’t help so you’re left with either making extra functions like bool isAggregating(classname) or reflection.   Admittedly magic is nice, and even if a isAggregating() method were to solve all the problems, I would strongly advise to stay away from such temptations.

To quote Matt Zandstra,

Magic is nice but clarity is much nicer.

Downloads

aggregation.zip
Here’s a zip file containing all the classes mentioned above plus support for properties.   It’s perhaps worth mentioning that this really isn’t production-ready, should you ever really want to use such a thing it will need a few tweaks first – namely how to handle adding two classes which define methods of the same name – which one has priority?   No doubt you’ll also want some inspection methods such as isAggregating() as previously mentioned.

This entry was posted in Object oriented programming, PHP and tagged , , , , , , , . Bookmark the permalink.

5 Responses to Transparent aggregation in PHP5

  1. tully says:

    there are 2 main differences betwen ruby mixin and your implementation;
    1. it’s nescessary to extend Aggregatable class
    2. $this in agregated classes references to this classes, not the CasualWorker

    so its’s not same as acts_as mixins where included modules (methods) became “real” part of class in which you using it.

    tully
    (the Ruby developer :) )

  2. will says:

    I’d like to try this, but the link to aggregation.zip is broken.

  3. Nick says:

    Thanks for pointing that out, I’ve fixed the link so it should be ok now.

  4. den says:

    One huge issue with this implementation is that you are aggregating two classes that have method with same name, one added (aggregated) first will fire. Nice try. Luckily there is an alternative for this in PHP6.

  5. Gustavo Sánchez says:

    Excellent!!! Many thanks

Leave a Reply

Your email address will not be published. Required fields are marked *