Ver índice de contenidos del libro

17.1. Eventos

PHP no soporta la herencia múltiple, lo que significa que una clase no puede heredar más que de otra clase. Además, no se pueden añadir nuevos métodos a una clase ya existente y tampoco se pueden redefinir sus métodos. Para paliar estas dos limitaciones y para ser un framework fácilmente modificable, Symfony proporciona un sistema de eventos inspirado en el centro de notificación de Cocoa, que a su vez se basa en el patrón de diseño Observer.

17.1.1. Comprendiendo los eventos

Algunas clases de Symfony notifican eventos durante la ejecución de la aplicación. Cuando un usuario modifica por ejemplo su cultura, el objeto que gestiona al usuario notifica que se ha producido un evento de tipo change_culture. Explicándolo sin palabras técnicas, esta notificación es como si el objeto le dijera al proyecto "Acabo de cambiar la cultura del usuario. Si necesitas hacer algo al respecto, este es el momento".

Cuando se produce un evento, la aplicación puede responder realizando cualquier proceso. La aplicación podría por ejemplo guardar la cultura del usuario en la base de datos cada vez que se produce el evento change_culture. Para responder a los eventos, tienes que registrar un event listener, que es la función que se va a ejecutar cada vez que se produzca el evento. El listado 17-1 muestra cómo registrar un listener que responda al evento change_culture del usuario:

Listado 17-1 - Registrando un event listener

$dispatcher->connect('user.change_culture', 'modificaCulturaUsuario');
 
function modificaCulturaUsuario(sfEvent $evento)
{
  $usuario = $evento->getSubject();
  $cultura = $evento['culture'];
 
  // Código que utiliza la cultura del usuario
}

La gestión de los eventos y el registro de listeners se gestiona mediante un objeto especial llamado event dispatcher. Este objeto está disponible en cualquier parte del código de la aplicación mediante el singleton sfContext y la mayoría de objetos de Symfony incluyen un método llamado getEventDispatcher() que permite tener acceso directo a ese objeto. El método connect() del dispatcher se utiliza para registrar cualquier elemento ejecutable de PHP (el método de una clase o una función) de forma que se ejecute cada vez que se produzca el evento. El primer argumento de connect() es el identificador del evento, que es una cadena de texto formada por un namespace y el nombre del evento. El segundo argumento es el nombre del elemento ejecutable de PHP.

Nota Código necesario para obtener el event dispatcher en cualquier parte de la aplicación

$dispatcher = sfContext::getInstance()->getEventDispatcher();

Las funciones registradas con el event dispatcher simplemente esperan a que se produzca el evento para el que han sido registradas. El event dispatcher guarda un registro de todos los listeners para saber cuáles se deben ejecutar cuando se notifique un evento. Cuando se ejecutan estos métodos o funciones, el dispatcher les pasa como argumento un objeto de tipo sfEvent.

El objeto del evento almacena información sobre el evento que ha sido notificado. El elemento que ha notificado el evento se puede obtener mediante el método getSubject() y los parámetros del evento se pueden acceder mediante el propio objeto del evento utilizando la sintaxis de los arrays. Para obtener por ejemplo el parámetro culture de sfUser cuando se notifica el evento user.change_culture, se utiliza $evento['culture'].

En resumen, el sistema de eventos permite añadir nuevas opciones a las clases existentes e incluso permite modificar sus métodos en tiempo de ejecución sin necesidad de utilizar la herencia de clases.

Nota Symfony 1.0 utiliza un mecanismo similar pero con una sintaxis muy diferente. En vez de realizar llamadas a los métodos del event dispatcher, en Symfony 1.0 se realizan llamadas a métodos estáticos de la clase sfMixer para registrar y notificar eventos. Aunque las llamadas a sfMixer se han declarado obsoletas, todavía funcionan correctamente en Symfony 1.2.

17.1.2. Notificando un evento

De la misma forma que las clases de Symfony notifican sus eventos, puedes hacer que tus clases sean fácilmente modificables notificando algunos de sus eventos más importantes. Imagina que tu aplicación realiza peticiones a varios servicios web externos y que has creado una clase llamada sfRestRequest para encapsular toda la lógica de tipo REST de estas peticiones. Una buena práctica consiste en notificar un evento cada vez que la clase realice una nueva petición. De esta forma, en el futuro será mucho más fácil añadirle funcionalidades como una cache y un sistema de logs. El listado 17-2 muestra el código que es necesario añadir a un método existente llamado obtener() para que notifique un evento.

Listado 17-2 - Notificando un evento

class sfRestRequest
{
  protected $dispatcher = null;
 
  public function __construct(sfEventDispatcher $dispatcher)
  {
    $this->dispatcher = $dispatcher;
  }
 
  /**
   * Realiza una petición a un servicio web externo
   */
  public function obtener($uri, $parametros = array())
  {
    // Notificar al dispatcher el inicio de la petición
    $this->dispatcher->notify(new sfEvent($this, 'peticion_rest.preparar_peticion', array(
      'uri'        => $uri,
      'parameters' => $parametros
    )));
 
    // Realizar la petición y guardar el resultado en una variable llamada $resultado
    // ...
 
    // Notificar al dispatcher la finalización de la petición
    $this->dispatcher->notify(new sfEvent($this, 'peticion_rest.peticion_finalizada', array(
      'uri'        => $uri,
      'parametros' => $parametros,
      'resultado'  => $resultado
    )));
 
    return $resultado;
  }
}

El método notify() del event dispatcher requiere como argumento un objeto de tipo sfEvent, el mismo tipo de objeto que se pasa a los event listeners. Este objeto siempre incluye una referencia al elemento que realiza la notificación (ese es el motivo por el que la instancia del objeto se inicializa con $this) y un identificador del evento. De forma opcional también admite un array asociativo de parámetros que permite a los listeners interactuar con la lógica del notificador del evento.

Nota Solamente las clases que notifican eventos se pueden modificar mediante el sistema de eventos. Por lo tanto, aunque no estés seguro de si en el futuro necesitarás modificar una clase en tiempo de ejecución, es conveniente que añadas notificaciones en al menos los métodos principales de tus clases.

17.1.3. Notificando un evento hasta que lo procese un listener

El método notify() asegura que todos los listeners registrados para un evento se van a ejecutar cuando se produzca el evento. Sin embargo, en ocasiones es necesario que un listener impida la notificación del evento de forma que ya no se ejecute ninguno de los restantes listeners registrados para ese evento. En este último caso se utiliza el método notifyUntil() en vez de notify(). De esta forma, el dispatcher ejecuta todos los listeners hasta que alguno de ellos devuelva un valor true y detenga la notificación del evento. Explicándolo sin palabras técnicas, este evento es como si el listener le dijera al proyecto "Ya me encargo yo de responder a este evento, por lo que no se lo notifiques a nadie más". El listado 17-3 muestra cómo utilizar esta técnica junto con el método mágico __call() para añadir métodos en tiempo de ejecución a una clase existente.

Listado 17-3 - Notificando un evento hasta que un listener devuelva true

class sfRestRequest
{
  // ...
 
  public function __call($metodo, $argumentos)
  {
    $evento = $this->dispatcher->notifyUntil(new sfEvent($this, 'peticion_rest.metodo_no_disponible', array(
      'metodo'     => $metodo, 
      'argumentos' => $argumentos
    )));
    if (!$evento->isProcessed())
    {
      throw new sfException(sprintf('Se ha invocado un método que no existe %s::%s.', get_class($this), $metodo));
    }
 
    return $evento->getReturnValue();
  }
}

Un event listener que se haya suscrito al evento peticion_rest.metodo_no_disponible puede comprobar el $metodo invocado para decidir si se encarga de el o decide pasarlo al siguiente event listener. El listado 17-4 muestra como una clase externa añade los métodos put() y delete() en la clase sfRestRequest en tiempo de ejecución utilizando este truco.

Listado 17-4 - Manejando un evento de tipo notifyUntil

class frontendConfiguration extends sfApplicationConfiguration
{
  public function configure()
  {
    // ...
 
    // Registrar el listener
    $this->dispatcher->connect('peticion_rest.metodo_no_disponible', array('sfRestRequestExtension', 'listenerMetodoNoDisponible'));
  }
}
 
class sfRestRequestExtension
{
  static public function listenerMetodoNoDisponible(sfEvent $evento)
  {
    switch ($evento['metodo'])
    {
      case 'put':
        self::put($evento->getSubject(), $evento['argumentos'])
 
        return true;
      case 'delete':
        self::delete($evento->getSubject(), $evento['argumentos'])
 
        return true;
      default:
        return false;
    }
  }
 
  static protected function put($peticionREST, $argumentos)
  {
    // Realizar la petición PUT y guardar el resultado en la variable $resultado
    // ...
 
    $evento->setReturnValue($resultado);
  }
 
  static protected function delete($peticionREST, $argumentos)
  {
    // Realizar la petición DELETE y guardar el resultado en la variable $resultado
    // ...
 
    $evento->setReturnValue($resultado);
  }
}

En la práctica, el método notifyUntil() hace posible la herencia múltiple de clases en PHP, en concreto mediante los mixins, que consisten en añadir métodos de varias clases en otra clase existente. Ahora es posible inyectar, en tiempo de ejecución, nuevos métodos en los objetos que no se pueden modificar mediante la herencia de clases. Lo mejor de todo es que si utilizas Symfony ya no estás limitado por las características orientadas a objetos de PHP.

Nota Como el primer listener que se encarga de un evento de tipo notifyUntil() evita que el evento siga notificándose, es importante conocer el orden en el que se ejecutan los listeners. El orden que se sigue es el mismo en el que fueron registrados, por lo que el primer listener registrado es el primer listener que se ejecuta.

En la práctica es difícil que el orden en el que se ejecutan los listeners sea un problema. Por lo tanto, si crees que dos listeners pueden entrar en conflicto para un determinado evento, es probable que tu clase tenga que notificar varios eventos, por ejemplo uno al principio y otro al final de la ejecución del método.

Por último, si los eventos añaden nuevos métodos a las clases existentes, utiliza nombres únicos de forma que no entren en conflicto con otros métodos añadidos en tiempo de ejecución. Una buena práctica en este sentido consiste en prefijar el nombre de los métodos con el nombre de la clase del listener.

17.1.4. Modificando el valor de retorno de un método

Obviamente, los listener no sólo pueden utilizar la información que reciben desde el evento, sino que también la pueden modificar para alterar la lógica original del notificador del evento. Para conseguirlo, se utiliza el método filter() del event dispatcher en vez del método notify(). En este caso, todos los listeners se invocan con dos parámetros: el objeto que representa al evento y el valor que se va a filtrar. Los listeners deben devolver un valor, que puede ser el mismo o completamente diferente. El listado 17-5 muestra cómo utilizar el método filter() para filtrar la respuesta recibida de un servicio web de modo que se puedan procesar los caracteres especiales.

Listado 17-5 - Notificando y procesando un evento con filtro

class sfRestRequest
{
  // ...
 
  /**
   * Realiza una petición a un servicio web externo
   */
  public function obtener($uri, $parametros = array())
  {
    // Realizar la petición y guardar el resultado en una variable llamada $resultado
    // ...
 
    // Notificar la finalización de la petición
    return $this->dispatcher->filter(new sfEvent($this, 'peticion_rest.filtrar_respuesta', array(
      'uri'        => $uri,
      'parametros' => $parametros,
    ), $resultado));
  }
}
 
// Aplicar el mecanismo de escape a la respuesta del servicio web
$dispatcher->connect('peticion_rest.filtrar_respuesta', 'rest_htmlspecialchars');
 
function rest_htmlspecialchars(sfEvent $evento, $resultado)
{
  return htmlspecialchars($resultado, ENT_QUOTES, 'UTF-8');
}

17.1.5. Eventos predefinidos

Muchas clases de Symfony incluyen varios eventos, lo que permite modificar las funcionalidades del framework sin tener que modificar sus clases. La tabla 17-1 muestra un listado completo de todos estos eventos junto con su tipo y sus argumentos.

Tabla 17-1 - Eventos de Symfony

Namespace Nombre Tipo Notificadores Argumentos
application log notify Muchas clases prioridad
application throw_exception notifyUntil sfException -
command log notify Las clases sfCommand* prioridad
command pre_command notifyUntil sfTask argumentos, opciones
command post_command notify sfTask -
command filter_options filter sfTask command_manager
configuration method_not_found notifyUntil sfProjectConfiguration método, argumentos
component method_not_found notifyUntil sfComponent método, argumentos
context load_factories notify sfContext -
controller change_action notify sfController módulo, acción
controller method_not_found notifyUntil sfController método, argumentos
controller page_not_found notify sfController módulo, acción
plugin pre_install notify sfPluginManager canal, plugin, is_package
plugin post_install notify sfPluginManager canal, plugin
plugin pre_uninstall notify sfPluginManager canal, plugin
plugin post_uninstall notify sfPluginManager canal, plugin
request filter_parameters filter sfWebRequest path_info
request method_not_found notifyUntil sfRequest método, argumentos
response method_not_found notifyUntil sfResponse método, argumentos
response filter_content filter sfResponse -
routing load_configuration notify sfRouting -
task cache.clear notifyUntil sfCacheClearTask aplicación, tipo, entorno
template filter_parameters filter sfViewParameterHolder -
user change_culture notify sfUser cultura
user method_not_found notifyUntil sfUser método, argumentos
user change_authentication notify sfBasicSecurityUser autenticado
view configure_format notify sfView formato, respuesta, petición
view method_not_found notifyUntil sfView método, argumentos
view.cache filter_content filter sfViewCacheManager respuesta, uri, nuevo

Puedes registrar todos los listeners que necesites para cada uno de los eventos predefinidos. Lo único que debes tener en cuenta es que los métodos o funciones PHP que registres deben devolver un valor booleano para los eventos de tipo notifyUntil y deben devolver el valor filtrado en los eventos de tipo filter.

Como se puede comprobar en la tabla anterior, los espacios de nombres o namespaces de los eventos no siempre coinciden con la función de la clase. Por ejemplo todas las clases de Symfony notifican el evento application.log cuando quieren guardar algo en los archivos de log (y también en la barra de depuración web):

$dispatcher->notify(new sfEvent($this, 'application.log', array($mensaje)));

Las clases propias de tu proyecto también pueden notificar eventos de Symfony siempre que lo necesiten.

17.1.6. ¿Dónde se registran los listeners?

Los event listeners se deben registrar lo antes posible durante la ejecución de una petición. En la práctica, el mejor sitio para registrar los event listeners es la clase de configuración de la aplicación. Esta clase dispone de una referencia al event dispatcher que se puede utilizar en el método configure(). El listado 17-6 muestra cómo registrar un listener para uno de los eventos de tipo peticion_rest de los ejemplos anteriores.

Listado 17-6 - Registrando un listener en la clase de configuración de la aplicación, en apps/frontend/config/ApplicationConfiguration.class.php

class frontendConfiguration extends sfApplicationConfiguration
{
  public function configure()
  {
    $this->dispatcher->connect('peticion_rest.metodo_no_disponible', array('sfRestRequestExtension', 'listenerMetodoNoDisponible'));
  }
}

Los plugins, que se explican más adelante en este capítulo, pueden registrar sus propios event listeners en el script config/config.php de cada plugin. Este script se ejecuta durante la inicialización de la aplicación y permite acceder al event dispatcher mediante $this->dispatcher.