Ver índice de contenidos del libro

17.1. Mixins

Una de las limitaciones actuales de PHP más molestas es que una clase no puede heredar de más de una clase. Además, tampoco se pueden añadir nuevos métodos a una clase ya existente y no se pueden redefinir los métodos existentes. Para paliar estas dos limitaciones y para hacer el framework realmente modificable, Symfony proporciona una clase llamada sfMixer. Su nombre viene del concepto de mixin utilizado en la programación orientada a objetos. Un mixin es un grupo de métodos o funciones que se juntan en una clase para que otras clases hereden de ella.

17.1.1. Comprendiendo la herencia múltiple

La herencia múltiple es la cualidad por la que una clase hereda de varias clases a la vez, heredando todas sus propiedades y métodos. A continuación se utiliza el ejemplo de una clase llamada Story (historia, relato) y otra clase llamada Book (libro), cada una de las cuales tiene sus propios métodos y propiedades, que se muestran en el listado 17-1.

Listado 17-1 - Dos clases de ejemplo

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;
  }
}

Una clase llamada ShortStory (relato corto) hereda de Story, una clase ComputerBook (libro sobre informática) hereda de Book, y como es lógico, una clase llamada Novel (novela) debería heredar tanto de Story como de Book para aprovechar todos sus métodos. Desafortunadamente, PHP no permite realizar esta herencia múltiple. No es posible crear una declaración para la clase Novel como la que se muestra en el listado 17-2.

Listado 17-2 - PHP no permite la herencia múltiple

class Novel extends Story, Book
{
}
 
$myNovel = new Novel();
$myNovel->getISBN();

Una posibilidad para este ejemplo es que la clase Novel implemente dos interfaces en vez de heredar de dos clases, pero esta solución implica que las clases padre no pueden contener código en los métodos que definen.

17.1.2. Clases de tipo mixing

La clase sfMixer intenta solucionar este problema desde otro punto de vista, permitiendo heredar de una clase a posteriori , siempre que la clase esté diseñada de forma adecuada. El proceso completo está formado por 2 pasos:

  • Declarar que una clase puede heredar de otras
  • Registrar las herencias realizadas (o mixins), después de la declaración de la clase

El listado 17-3 muestra cómo implementar la clase Novel anterior mediante sfMixer.

Listado 17-3 - sfMixer permite la herencia múltiple

class Novel extends Story
{
  public function __call($method, $arguments)
  {
    return sfMixer::callMixins();
  }
}
 
sfMixer::register('Novel', array('Book', 'getISBN'));
$miNovela = new Novel();
$miNovela->getISBN();

Una de las clases que se quieren heredar (en este caso, Story) se utiliza como clase padre principal, de forma que la clase hereda de ella directamente mediante PHP. La clase Novel se declara que es extensible mediante el código del método __call(). El método de la otra clase de la que se quiere heredar (en este caso, Book) se añade posteriormente a la clase Novel mediante la llamada a sfMixer::register(). Las próximas secciones detallan el funcionamiento completo de este proceso.

Cuando se invoca el método getISBN() de la clase Novel, el funcionamiento es idéntico a si la clase Novel hubiera heredado también de la otra clase (como en el listado 17-2), salvo que en este caso, el funcionamiento se debe a la magia del método __call() y a los métodos estáticos de la clase sfMixer. El método getISBN() se dice que ha sido mezclado (en inglés, "mixed", y de ahí el nombre mixin) en la clase Novel.

17.1.3. Declarar que una clase se puede extender

Para declarar que una clase puede ser extendida, se debe preparar su código de forma que la clase sfMixer pueda identicarla como tal. Para preparar la clase, se añaden lo que se denomina "hooks", que en este caso consisten en llamadas al método sfMixer::callMixins(). Muchas de las clases propias de Symfony ya incluyen estos hooks, entre otras, sfRequest, sfResponse, sfController, sfUser y sfAction.

En función del grado hasta el que se quiere hacer extensible a una clase, los hooks se colocan en diferentes partes de la clase.:

  • Para permitir que se puedan añadir métodos a una clase, se inserta el hook en el método __call() y se devuelve su valor, como se muestra en el listado 17-4.

Listado 17-4 - Permitiendo que se puedan añadir nuevos métodos a una clase

class unaClase
{
  public function __call($method, $arguments)
  {
    return sfMixer::callMixins();
  }
}
  • Para permitir modificar la forma en la que funciona un método, se debe insertar el hook dentro de ese método, como muestra el listado 17-5. El código que añade la clase de tipo mixin se ejecuta en el mismo lugar en el que se encuentra el hook.

Listado 17-5 - Permitiendo que se pueda modificar un método

class otraClase
{
  public function unMetodo()
  {
    echo "Haciendo cosas...";
    sfMixer::callMixins();
  }
}

En ocasiones es necesario insertar más de un hook en un método. En este caso, se debe asignar un nombre a cada hook, de forma que posteriormente se pueda definir el hook que se quiere utilizar, tal y como muestra el listado 17-6. Para crear un hook con nombre, se utiliza el mismo método callMixins(), pero con un argumento que indica el nombre del hook. Cuando se registra el mixin posteriormente, se utiliza este nombre para indicar en que lugar del método se debe ejecutar el código del mixin.

Listado 17-6 - Si un método contiene más de un hook, se les debe asignar un nombre

class otraClase
{
  public function otroMetodo()
  {
    echo "Empezando...";
    sfMixer::callMixins('comienzo');
    echo "Haciendo cosas...";
    sfMixer::callMixins('fin');
    echo "Finalizado";
  }
}

Como muestra el listado 17-7, se pueden combinar todas estas técnicas para crear clases con la habilidad de poder insertar y/o modificar sus métodos.

Listado 17-7 - Extendiendo una clase de diversas formas a la vez

class BicycleRider
    {
      protected $name = 'John';
 
      public function getName()
      {
        return $this->name;
      }
 
      public function sprint($distance)
      {
        echo $this->name." sprints ".$distance." meters\n";
        sfMixer::callMixins(); // El método sprint() se puede extender
      }
 
      public function climb()
      {
        echo $this->name.' climbs';
        sfMixer::callMixins('slope'); // El método climb() se puede extender aquí...
        echo $this->name.' gets to the top';
        sfMixer::callMixins('top'); // ...y en este otro punto también
      }
 
      public function __call($method, $arguments)
      {
        return sfMixer::callMixins(); // La clase BicyleRider se puede extender
      }
    }

Advertencia sfMixer solamente puede extender las clases que lo han declarado de forma explícita. Por tanto, no se puede utilizar este mecanismo para modificar una clase que no lo ha indicado en su código. En otras palabras, es como si las clases que quieren utilizar los servicios de sfMixer debieran suscribirse antes a esos servicios.

17.1.4. Registrando las extensiones

Para registrar una extensión en un hook previamente definido, se utiliza el método sfMixer::register(). El primer argumento de este método es el elemento que se va a extender y el segundo argumento es una función de PHP que representa el mixin que se va a incluir.

El formato del primer argumento depende de lo que se quiere extender:

  • Si se extiende una clase, se utiliza el nombre de la clase.
  • Si se extiende un método con un hook sin nombre, se utiliza el patrón clase:metodo.
  • Si se extiende un método con un hook que dispone de nombre, se utiliza el patrón clase:metodo:hook.

El listado 17-8 ilustra estas normas extendiendo la clase que se definió en el listado 17-7. El objeto que se extiende se pasa automáticamente como el primer argumento a los métodos del mixin (salvo, evidentemente, si el método que se ha extendido es de tipo static). El método del mixin también puede acceder a los parámetros de la llamada al método original.

Listado 17-8 - Registrando extensiones

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!

Le mecanismo de extensiones no solo permite añadir nuevos métodos. El método partyAllNight() anterior utiliza un atributo de la clase Steroids. Por tanto, cuando se extiende la clase BicycleRider con un método de la clase Steroids, en realidad se está creando una nueva instancia de la clase Steroids dentro del objeto BicycleRider.

Advertencia No se pueden añadir 2 métodos con el mismo nombre a una clase ya existente. La razón es que la llamada a callMixins() en los métodos __call() utiliza el nombre del método del mixin como una clave. Además, no se puede añadir un método a una clase que ya dispone de un método con el mismo nombre, ya que el mecanismo de mixin depende del método mágico __call(), por lo que en este caso, nunca se llamaría a este segundo método.

El segundo argumento de la llamada al método register() debe ser cualquier elemento PHP que se pueda invocar, por lo que puede ser un array de clase::metodo, un array de objeto->metodo o incluso el nombre de una función. El listado 17-9 muestra algunos ejemplos:

Listado 17-9 - Cualquier código PHP que se pueda invocar puede ser utilizado para registrar una extensión

// Registrnado el método de una clase
sfMixer::register('BicycleRider', array('Steroids', 'partyAllNight'));
 
// Registrando el método de un objeto
$mySteroids = new Steroids();
sfMixer::register('BicycleRider', array($mySteroids, 'partyAllNight'));
 
// Registrando una función
sfMixer::register('BicycleRider', 'die');

El mecanismo de extensión es dinámico, lo que significa que si ya se ha instanciado un objeto, puede tener acceso a las extensiones realizadas en su clase. El listado 17-10 muestra un ejemplo.

Listado 17-10 - El mecanismo de extensión es dinámico y se puede utilizar incluso después de instanciar los objetos

$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. Extendiendo de forma más precisa

La instrucción sfMixer::callMixins() en realidad es un atajo de algo mucho más complejo. Esta instrucción recorre todos los elementos de la lista de mixins que se han registrado y se van ejecutando uno a uno, pasandoles el objeto actual y los parámetros del método que se está ejecutando. En otras palabras, una llamada a la función sfMixer::callMixins() se comporta más o menos como el código del listado 17-11.

Listado 17-11 - callMixin() recorre todos los mixins registrados y los ejecuta

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

Si se necesitan pasar otros parámetros o se quiere procesar de forma especial el valor devuelto, se puede recorrer la lista de mixins de forma explícita en lugar de utilizar sfMixer::callMixins(). El listado 17-12 muestra un ejemplo de un mixin más integrado en la propia clase.

Listado 17-12 - Reemplazando callMixin() por un código propio

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'));