Among the current limitations of PHP, one of the most annoying is you can't have a class extend more than one class. Another limitation is you can't add new methods to an existing class or override existing methods. To palliate these two limitations and to make the framework truly extendable, symfony introduces a class called sfMixer. It is in no way related to cooking devices, but to the concept of mixins found in object-oriented programming. A mixin is a group of methods or functions that can be mixed into a class to extend it.

17.1.1. Understanding Multiple Inheritance

Multiple inheritance is the ability for a class to extend more than one class and inherit these class properties and methods. Let's consider an example. Imagine a Story and a Book class, each with its own properties and methods — just like in Listing 17-1.

Listing 17-1 - Two Example Classes

class Story
{
  protected $title = '';
  protected $topic = '';
  protected $characters = array();

  public function __construct($title = `, $topic = `, $characters = array())
  {
    $this->title = $title;
    $this->topic = $topic;
    $this->characters = $characters;
  }

  public function getSummary()
  {
    return $this->title.', a story about '.$this->topic;
  }
}

class Book
{
  protected $isbn = 0;

  function setISBN($isbn = 0)
  {
    $this->isbn = $isbn;
  }

  public function getISBN()
  {
    return $this->isbn;
  }
}

A ShortStory class extends Story, a ComputerBook class extends Book, and logically, a Novel should extend both Story and Book and take advantage of all their methods. Unfortunately, this is not possible in PHP. You cannot write the Novel declaration as in Listing 17-2.

Listing 17-2 - Multiple Inheritance Is Not Possible in PHP

class Novel extends Story, Book
{
}

$myNovel = new Novel();
$myNovel->getISBN();

One possibility would be to have Novel implements two interfaces instead of having it extend two classes, but this would prevent you from having the methods actually written in the parent classes.

17.1.2. Mixing Classes

The sfMixer class takes another approach to the problem, taking an existing class and extending it a posteriori, provided that the class contains the proper hooks. The process involves two steps:

  • Declaring a class as extendable
  • Registering extensions (or mixins), after the class declaration

Listing 17-3 shows how you would implement the Novel class with sfMixer.

Listing 17-3 - Multiple Inheritance Is Possible via sfMixer

class Novel extends Story
{
  public function __call($method, $arguments)
  {
    return sfMixer::callMixins();
  }
}

sfMixer::register('Novel', array('Book', 'getISBN'));
$myNovel = new Novel();
$myNovel->getISBN();

One of the classes (Story) is chosen as the main parent, in line with PHP's ability to only inherit from one class. The Novel class is declared as extendable by the code located in the __call() method. The method of the other class (Book) is added afterwards to the Novel class by a call to sfMixer::register(). The next sections will explicitly explain this process.

When the getISBN() method of the Novel class is called, everything happens as if the class had been defined as in Listing 17-2--except it's the magic of the __call() method and of the sfMixer static methods that simulate it. The getISBN() method is mixed in the Novel class.

17.1.3. Declaring a Class As Extendable

To declare a class as extendable, you must insert one or several "hooks" into the code, which the sfMixer class can later identify. These hooks are calls to the sfMixer::callMixins() method. Many of the symfony classes already contain such hooks, including sfRequest, sfResponse, sfController, sfUser, sfAction, and others.

The hook can be placed in different parts of the class, according to the desired degree of extensibility:

  • To be able to add new methods to a class, you must insert the hook in the __call() method and return its result, as demonstrated in Listing 17-4.

Listing 17-4 - Giving a Class the Ability to Get New Methods

class SomeClass
{
  public function __call($method, $arguments)
  {
    return sfMixer::callMixins();
  }
}
  • To be able to alter the way an existing method works, you must insert the hook inside the method, as demonstrated in Listing 17-5. The code added by the mixin class will be executed where the hook is placed.

Listing 17-5 - Giving a Method the Ability to Be Altered

class SomeOtherClass
{
  public function doThings()
  {
    echo "I'm working...";
    sfMixer::callMixins();
  }
}

You may want to place more than one hook in a method. In this case, you must name the hooks, so that you can define which hook is to be extended afterwards, as demonstrated in Listing 17-6. A named hook is a call to callMixins() with a hook name as a parameter. This name will be used afterwards, when registering a mixin, to tell where in the method the mixin code must be executed.

Listing 17-6 - A Method Can Contain More Than One Hook, In Which Case They Must Be Named

class AgainAnotherClass
{
  public function doMoreThings()
  {
    echo "I'm ready.";
    sfMixer::callMixins('beginning');
    echo "I'm working...";
    sfMixer::callMixins('end');
    echo "I'm done.";
  }
}

Of course, you can combine these techniques to create classes with the ability to be assigned new and extendable methods, as Listing 17-7 demonstrates.

Listing 17-7 - A Class Can Be Extendable in Various Ways

class BicycleRider
{
  protected $name = 'John';

  public function getName()
  {
    return $this->name;
  }

  public function sprint($distance)
  {
    echo $this->name." sprints ".$distance." meters\n";
    sfMixer::callMixins(); // The sprint() method is extendable
  }

  public function climb()
  {
    echo $this->name.' climbs';
    sfMixer::callMixins('slope'); // The climb() method is extendable here
    echo $this->name.' gets to the top';
    sfMixer::callMixins('top'); // And also here
  }

  public function __call($method, $arguments)
  {
    return sfMixer::callMixins(); // The BicyleRider class is extendable
  }
}

Caution Only the classes that are declared as extendable can be extended by sfMixer. This means that you cannot use this mechanism to extend a class that didn't "subscribe" to this service.

17.1.4. Registering Extensions

To register an extension to an existing hook, use the sfMixer::register() method. Its first argument is the element to extend, and the second argument is a PHP callable and represents the mixin.

The format of the first argument depends on what you try to extend:

  • If you extend a class, use the class name.
  • If you extend a method with an anonymous hook, use the class:method pattern.
  • If you extend a method with a named hook, use the class:method:hook pattern.

Listing 17-8 illustrates this principle by extending the class defined in Listing 17-7. The extended object is automatically passed as first parameter to the mixin methods (except, of course, if the extended method is static). The mixin method also gets access to the parameters of the original method call.

Listing 17-8 - Registering Extensions

class Steroids
{
  protected $brand = 'foobar';

  public function partyAllNight($bicycleRider)
  {
    echo $bicycleRider->getName()." spends the night dancing.\n";
    echo "Thanks ".$brand."!\n";
  }

  public function breakRecord($bicycleRider, $distance)
  {
    echo "Nobody ever made ".$distance." meters that fast before!\n";
  }

  static function pass()
  {
    echo " and passes half the peloton.\n";
  }
}

sfMixer::register('BicycleRider', array('Steroids', 'partyAllNight'));
sfMixer::register('BicycleRider:sprint', array('Steroids', 'breakRecord'));
sfMixer::register('BicycleRider:climb:slope', array('Steroids', 'pass'));
sfMixer::register('BicycleRider:climb:top', array('Steroids', 'pass'));

$superRider = new BicycleRider();
$superRider->climb();
=> John climbs and passes half the peloton
=> John gets to the top and passes half the peloton
$superRider->sprint(2000);
=> John sprints 2000 meters
=> Nobody ever made 2000 meters that fast before!
$superRider->partyAllNight();
=> John spends the night dancing.
=> Thanks foobar!

The extension mechanism is not only about adding methods. The partyAllNight() method uses an attribute of the Steroids class. This means that when you extend the BicycleRider class with a method of the Steroids class, you actually create a new Steroids instance inside the BicycleRider object.

Caution You cannot add two methods with the same name to an existing class. This is because the callMixins() call in the __call() methods uses the mixin method name as a key. Also, you cannot add a method to a class that already has a method with the same name, because the mixin mechanism relies on the magic __call() method and, in that particular case, it would never be called.

The second argument of the register() call is a PHP callable, so it can be a class::method array, or an object->method array, or even a function name. See examples in Listing 17-9.

Listing 17-9 - Any Callable Can Be Registered As a Mixer Extension

// Use a class method as a callable
sfMixer::register('BicycleRider', array('Steroids', 'partyAllNight'));

// Use an object method as a callable
$mySteroids = new Steroids();
sfMixer::register('BicycleRider', array($mySteroids, 'partyAllNight'));

// Use a function as a callable
sfMixer::register('BicycleRider', 'die');

The extension mechanism is dynamic, which means that even if you already instantiated an object, it can take advantage of further extensions in its class. See an example in Listing 17-10.

Listing 17-10 - The Extension Mechanism Is Dynamic and Can Occur Even After Instantiation

$simpleRider = new BicycleRider();
$simpleRider->sprint(500);
=> John sprints 500 meters
sfMixer::register('BicycleRider:sprint', array('Steroids', 'breakRecord'));
$simpleRider->sprint(500);
=> John sprints 500 meters
=> Nobody ever made 500 meters that fast before!

17.1.5. Extending with More Precision

The sfMixer::callMixins() instruction is actually a shortcut to something a little bit more elaborate. It automatically loops over the list of registered mixins and calls them one by one, passing to it the current object and the current method parameters. In short, an sfMixer::callMixins() call behaves more or less like Listing 17-11.

Listing 17-11 - callMixin() Loops Over the Registered Mixins and Executes Them

foreach (sfMixer::getCallables($class.':'.$method.':'.$hookName) as $callable)
{
  call_user_func_array($callable, $parameters);
}

If you want to pass other parameters or to do something special with the return value, you can write the foreach loop explicitly instead of using the shortcut method. Look at Listing 17-12 for an example of a mixin more integrated into a class.

Listing 17-12 - Replacing callMixin() by a Custom Loop

class Income
{
  protected $amount = 0;

  public function calculateTaxes($rate = 0)
  {
    $taxes = $this->amount * $rate;
    foreach (sfMixer::getCallables('Income:calculateTaxes') as $callable)
    {
      $taxes += call_user_func($callable, $this->amount, $rate);
    }

    return $taxes;
  }
}

class FixedTax
{
  protected $minIncome = 10000;
  protected $taxAmount = 500;

  public function calculateTaxes($amount)
  {
    return ($amount > $this->minIncome) ? $this->taxAmount : 0;
  }
}

sfMixer::register('Income:calculateTaxes', array('FixedTax', 'calculateTaxes'));