TigerZF
🌐Español

Capítulo 31. Zend_EventManager

31.1. El EventManager

31.1.1. Resumen

Zend_EventManager es un componente diseñado para los siguientes casos de uso:

  • Implementar patrones simples de sujeto/observador.

  • Implementar diseños orientados a aspectos.

  • Implementar arquitecturas dirigidas por eventos.

La arquitectura básica le permite adjuntar y separar escuchadores a eventos con nombre, tanto por instancia como de forma estática; disparar eventos; e interrumpir la ejecución de los escuchadores.

31.1.2. Inicio rápido

Normalmente, se compone una instancia de Zend_EventManager_EventManager en una clase.

class Foo
{
    protected $events;

    public function events(Zend_EventManager_EventCollection $events = null)
    {
        if (null !== $events) {
            $this->events = $events;
        } elseif (null === $this->events) {
            $this->events = new Zend_EventManager_EventManager(__CLASS__);
        }
        return $this->events;
    }
}

Lo anterior permite a los usuarios acceder a la instancia de EventManager, o restablecerla con una nueva instancia; si no existe ninguna, se instanciará de forma perezosa bajo demanda.

Un EventManager solo resulta interesante si dispara algún evento. El disparo básico toma tres argumentos: el nombre del evento, que suele ser el nombre de la función/método actual; el "contexto", que suele ser la instancia del objeto actual; y los argumentos, que suelen ser los argumentos proporcionados a la función/método actual.

class Foo
{
    // ... assume events definition from above

    public function bar($baz, $bat = null)
    {
        $params = compact('baz', 'bat');
        $this->events()->trigger(__FUNCTION__, $this, $params);
    }
}

A su vez, disparar eventos solo resulta interesante si algo está escuchando el evento. Los escuchadores se adjuntan al EventManager, especificando un evento con nombre y el callback a notificar. El callback recibe un objeto Zend_EventManager_Event, que dispone de accesores para recuperar el nombre del evento, el contexto y los parámetros. Añadamos un escuchador y disparemos el evento.

$log = Zend_Log::factory($someConfig);
$foo = new Foo();
$foo->events()->attach('bar', function ($e) use ($log) {
    $event  = $e->getName();
    $target = get_class($e->getTarget());
    $params = json_encode($e->getParams());

    $log->info(sprintf(
        '%s called on %s, using params %s',
        $event,
        $target,
        $params
    ));
});

// Results in log message:
$foo->bar('baz', 'bat');
// reading: bar called on Foo, using params {"baz" : "baz", "bat" : "bat"}"

Tenga en cuenta que el segundo argumento de attach() es cualquier callback válido; se muestra una función anónima en el ejemplo para mantener el ejemplo autocontenido. Sin embargo, también podría utilizar un nombre de función válido, un functor, una cadena que haga referencia a un método estático, o un callback de tipo array con un método estático con nombre o un método de instancia. De nuevo, cualquier callback de PHP es válido.

En ocasiones puede que quiera especificar escuchadores sin tener aún una instancia de objeto de la clase que compone un EventManager. El Zend_EventManager_StaticEventManager le permite hacer esto. La llamada a attach es idéntica a la del EventManager, pero espera un parámetro adicional al principio: una instancia con nombre. ¿Recuerda el ejemplo de composición de un EventManager, cómo le pasamos __CLASS__? Ese valor, o cualquier cadena que proporcione en un array al constructor, puede utilizarse para identificar una instancia al usar el StaticEventManager. Como ejemplo, podríamos cambiar el ejemplo anterior para adjuntar de forma estática:

$log = Zend_Log::factory($someConfig);
$events = Zend_EventManager_StaticEventManager::getInstance();
$events->attach('Foo', 'bar', function ($e) use ($log) {
    $event  = $e->getName();
    $target = get_class($e->getTarget());
    $params = json_encode($e->getParams());

    $log->info(sprintf(
        '%s called on %s, using params %s',
        $event,
        $target,
        $params
    ));
});

// Later, instantiate Foo:
$foo = new Foo();

// And we can still trigger the above event:
$foo->bar('baz', 'bat');
// results in log message:
// bar called on Foo, using params {"baz" : "baz", "bat" : "bat"}"

El EventManager también ofrece la posibilidad de separar escuchadores, cortocircuitar la ejecución de un evento ya sea desde dentro de un escuchador o comprobando los valores de retorno de los escuchadores, comprobar y recorrer en bucle los resultados devueltos por los escuchadores, priorizar escuchadores, y más. Muchas de estas características se detallan en los ejemplos.

31.1.2.1. Escuchadores comodín

A veces querrá adjuntar el mismo escuchador a muchos eventos o a todos los eventos de una instancia dada -- o incluso, con el gestor estático, a muchos contextos y muchos eventos. El componente EventManager permite esto.

Ejemplo 31.1. Adjuntar a muchos eventos a la vez

$events = new Zend_EventManager_EventManager();
$events->attach(array('these', 'are', 'event', 'names'), $callback);

Tenga en cuenta que si especifica una prioridad, esa prioridad se usará para todos los eventos especificados.


Ejemplo 31.2. Adjuntar usando el comodín

$events = new Zend_EventManager_EventManager();
$events->attach('*', $callback);

Tenga en cuenta que si especifica una prioridad, esa prioridad se usará para este escuchador en cualquier evento disparado.

Lo anterior especifica que cualquier evento disparado resultará en la notificación de este escuchador en particular.


Ejemplo 31.3. Adjuntar a muchos eventos a la vez mediante el StaticEventManager

$events = Zend_EventManager_StaticEventManager::getInstance();
// Attach to many events on the context "foo"
$events->attach('foo', array('these', 'are', 'event', 'names'), $callback);

// Attach to many events on the contexts "foo" and "bar"
$events->attach(array('foo', 'bar'), array('these', 'are', 'event', 'names'), $callback);

Tenga en cuenta que si especifica una prioridad, esa prioridad se usará para todos los eventos especificados.


Ejemplo 31.4. Adjuntar a muchos eventos a la vez mediante el StaticEventManager

$events = Zend_EventManager_StaticEventManager::getInstance();
// Attach to all events on the context "foo"
$events->attach('foo', '*', $callback);

// Attach to all events on the contexts "foo" and "bar"
$events->attach(array('foo', 'bar'), '*', $callback);

Tenga en cuenta que si especifica una prioridad, esa prioridad se usará para todos los eventos especificados.

Lo anterior especifica que para los contextos "foo" y "bar", el escuchador especificado será notificado para cualquier evento que disparen.


31.1.3. Opciones de configuración

Opciones de Zend_EventManager_EventManager

identifier

Una cadena o array de cadenas a las que la instancia de EventManager dada puede responder cuando se accede a través de el StaticEventManager.

event_class

El nombre de una clase alternativa de Zend_EventManager_Event para usar al representar los eventos pasados a los escuchadores.

static_connections

Una instancia de Zend_EventManager_StaticEventCollection para usar al disparar eventos. Por defecto, se usará la instancia global de Zend_EventManager_StaticEventManager, pero esto puede anularse pasando un valor a este método. Un valor null evitará que la instancia dispare más escuchadores adjuntados de forma estática.

31.1.4. Métodos disponibles

__construct
__construct(null|string|int $identifier);

Construye una nueva instancia de EventManager, usando el identificador dado, si se proporciona, con fines de adjunción estática.

setEventClass
setEventClass(string $class);

Proporciona el nombre de una clase alternativa de Zend_EventManager_Event para usar al crear eventos que se pasarán a los escuchadores disparados.

setStaticConnections
setStaticConnections(Zend_EventManager_StaticEventCollection $connections = null);

Una instancia de Zend_EventManager_StaticEventCollection para usar al disparar eventos. Por defecto, se usará la instancia global de Zend_EventManager_StaticEventManager, pero esto puede anularse pasando un valor a este método. Un valor null evitará que la instancia dispare más escuchadores adjuntados de forma estática.

getStaticConnections
getStaticConnections();

Devuelve la instancia de Zend_EventManager_StaticEventCollection actualmente adjunta, recuperando de forma perezosa la instancia global de Zend_EventManager_StaticEventManager si no hay ninguna adjunta y el uso de escuchadores estáticos no ha sido deshabilitado pasando un valor null a setStaticConnections(). Devuelve un valor booleano false si los escuchadores estáticos están deshabilitados, o una instancia de StaticEventCollection en caso contrario.

trigger
trigger(string $event, mixed $target, mixed $argv, callback $callback);

Dispara todos los escuchadores de un evento con nombre. Se recomienda usar el nombre de la función/método actual para $event, añadiéndole valores como ".pre", ".post", etc. según sea necesario. $context debería ser la instancia del objeto actual, o el nombre de la función si no se dispara dentro de un objeto. $params normalmente debería ser un array asociativo o una instancia de ArrayAccess; recomendamos usar los parámetros pasados a la función/método (compact() es a menudo útil aquí). Este método también puede recibir un callback y comportarse de la misma manera que triggerUntil().

El método devuelve una instancia de Zend_EventManager_ResponseCollection, que puede usarse para inspeccionar los valores de retorno de los distintos escuchadores, comprobar si hubo cortocircuito, y más.

triggerUntil
triggerUntil(string $event, mixed $context, mixed $argv, callback $callback);

Dispara todos los escuchadores de un evento con nombre, igual que trigger(), con la particularidad de que pasa el valor de retorno de cada escuchador a $callback; si $callback devuelve un valor booleano true, se interrumpe la ejecución de los escuchadores. Puede comprobar esto usando $result->stopped().

attach
attach(string $event, callback $callback, int $priority);

Adjunta $callback a la instancia de Zend_EventManager_EventManager, escuchando el evento $event. Si se proporciona una $priority, el escuchador se insertará en la pila interna de escuchadores usando esa prioridad; los valores más altos se ejecutan antes. (La prioridad por defecto es "1", y se permiten prioridades negativas.)

El método devuelve una instancia de Zend_Stdlib_CallbackHandler; este valor puede pasarse posteriormente a detach() si se desea.

attachAggregate
attachAggregate(string|Zend_EventManager_ListenerAggregate $aggregate);

Si se pasa una cadena para $aggregate, se instancia esa clase. Luego se pasa la instancia de EventManager a $aggregate mediante su método attach() para que pueda registrar escuchadores.

Se devuelve la instancia de ListenerAggregate.

detach
detach(Zend_Stdlib_CallbackHandler $listener);

Recorre todos los escuchadores y separa cualquiera que coincida con $listener para que ya no se disparen.

Devuelve un valor booleano true si se ha identificado y anulado la suscripción de algún escuchador, y un valor booleano false en caso contrario.

detachAggregate
detachAggregate(Zend_EventManager_ListenerAggregate $aggregate);

Recorre todos los escuchadores de todos los eventos para identificar a los escuchadores representados por el agregado; para todas las coincidencias, los escuchadores se eliminarán.

Devuelve un valor booleano true si se ha identificado y anulado la suscripción de algún escuchador, y un valor booleano false en caso contrario.

detachAggregate
getEvents();

Devuelve un array con todos los nombres de eventos que tienen escuchadores adjuntos.

getListeners
getListeners(string $event);

Devuelve una instancia de Zend_Stdlib_PriorityQueue con todos los escuchadores adjuntos a $event.

clearListeners
clearListeners(string $event);

Elimina todos los escuchadores adjuntos a $event.

prepareArgs
prepareArgs(array $args);

Crea un ArrayObject a partir del $args proporcionado. Esto puede resultar útil si desea que sus escuchadores puedan modificar los argumentos de modo que los escuchadores posteriores o el método que dispara el evento puedan ver los cambios.

31.1.5. Ejemplos

Ejemplo 31.5. Modificar argumentos

En ocasiones puede resultar útil permitir que los escuchadores modifiquen los argumentos que reciben, de modo que los escuchadores posteriores o el método invocador reciban esos valores modificados.

Como ejemplo, podría querer pre-filtrar una fecha que sabe que llegará como una cadena y convertirla en un argumento DateTime.

Para ello, puede pasar sus argumentos a prepareArgs(), y pasar este nuevo objeto al disparar un evento. Luego recuperará ese valor de vuelta en su método.

class ValueObject
{
    // assume a composed event manager

    function inject(array $values)
    {
        $argv = compact('values');
        $argv = $this->events()->prepareArgs($argv);
        $this->events()->trigger(__FUNCTION__, $this, $argv);
        $date = isset($argv['values']['date']) ? $argv['values']['date'] : new DateTime('now');

        // ...
    }
}

$v = new ValueObject();

$v->events()->attach('inject', function($e) {
    $values = $e->getParam('values');
    if (!$values) {
        return;
    }
    if (!isset($values['date'])) {
        $values['date'] = new DateTime('now');
        return;
    }
    $values['date'] = new Datetime($values['date']);
});

$v->inject(array(
    'date' => '2011-08-10 15:30:29',
));

Ejemplo 31.6. Cortocircuitar

Un caso de uso común para los eventos es disparar escuchadores hasta que uno indique que no se debe realizar más procesamiento, o hasta que un valor de retorno cumpla criterios específicos. Como ejemplo, si un evento crea un objeto Response, puede que se desee detener la ejecución.

$listener = function($e) {
    // do some work

    // Stop propagation and return a response
    $e->stopPropagation(true);
    return $response;
};

Alternativamente, podríamos hacer la comprobación desde el método que dispara el evento.

class Foo implements Dispatchable
{
    // assume composed event manager

    public function dispatch(Request $request, Response $response = null)
    {
        $argv = compact('request', 'response');
        $results = $this->events()->triggerUntil(__FUNCTION__, $this, $argv, function($v) {
            return ($v instanceof Response);
        });
    }
}

Normalmente, puede que desee devolver el valor que detuvo la ejecución, o usarlo de alguna manera. Tanto trigger() como triggerUntil() devuelven una instancia de Zend_EventManager_ResponseCollection; llame a su método stopped() para comprobar si la ejecución se detuvo, y al método last() para recuperar el valor de retorno del último escuchador ejecutado:

class Foo implements Dispatchable
{
    // assume composed event manager

    public function dispatch(Request $request, Response $response = null)
    {
        $argv = compact('request', 'response');
        $results = $this->events()->triggerUntil(__FUNCTION__, $this, $argv, function($v) {
            return ($v instanceof Response);
        });

        // Test if execution was halted, and return last result:
        if ($results->stopped()) {
            return $results->last();
        }

        // continue...
    }
}

Ejemplo 31.7. Asignar prioridad a los escuchadores

Un caso de uso para el EventManager es la implementación de sistemas de caché. Como tal, a menudo se desea comprobar la caché de forma temprana, y guardar en ella de forma tardía.

El tercer argumento de attach() es un valor de prioridad. Cuanto más alto sea este número, antes se ejecutará ese escuchador; cuanto más bajo sea, más tarde se ejecutará. El valor por defecto es 1, y los valores se dispararán en el orden en que se registraron dentro de una prioridad determinada.

Así, para implementar un sistema de caché, nuestro método necesitará disparar un evento al inicio del método así como al final del método. Al inicio del método, queremos un evento que se dispare de forma temprana; al final del método, un evento debería dispararse de forma tardía.

Aquí está la clase en la que queremos la caché:

class SomeValueObject
{
    // assume it composes an event manager

    public function get($id)
    {
        $params = compact('id');
        $results = $this->events()->trigger('get.pre', $this, $params);

        // If an event stopped propagation, return the value
        if ($results->stopped()) {
            return $results->last();
        }

        // do some work...

        $params['__RESULT__'] = $someComputedContent;
        $this->events()->trigger('get.post', $this, $params);
    }
}

Ahora, creemos un ListenerAggregate que pueda manejar la caché por nosotros:

class CacheListener implements Zend_EventManager_ListenerAggregate
{
    protected $cache;

    public function __construct(Cache $cache)
    {
        $this->cache = $cache;
    }

    public function attach(Zend_EventManager_EventCollection $events)
    {
        $events->attach('get.pre', array($this, 'load'), 100);
        $events->attach('get.post', array($this, 'save'), -100);
    }

    public function load($e)
    {
        $id = get_class($e->getTarget()) . '-' . json_encode($e->getParams());
        if (false !== ($content = $this->cache->load($id))) {
            $e->stopPropagation(true);
            return $content;
        }
    }

    public function save($e)
    {
        $params  = $e->getParams();
        $content = $params['__RESULT__'];
        unset($params['__RESULT__']);

        $id = get_class($e->getTarget()) . '-' . json_encode($params);
        $this->cache->save($content, $id);
    }
}

Podemos entonces adjuntar el agregado a una instancia.

$value         = new SomeValueObject();
$cacheListener = new CacheListener($cache);
$value->events()->attachAggregate($cacheListener);

Ahora, cuando llamemos a get(), si tenemos una entrada en caché, se devolverá inmediatamente; si no, se guardará en caché una entrada calculada al completar el método.