Symfony – The Event Dispatcher Component – Usando el Componente



Symfony – The Event Dispatcher Component – Usando el Componente

0 0


event_dispatcher

symfony - The Event Dispatcher Component

On Github franjs / event_dispatcher

Symfony

The Event Dispatcher Component

Francisco Silva - https://github.com/franjs

Este componente permite implementar el Patrón Observador mediante el uso de la clase (EventDispatcher) que contiene a los escuchas de eventos y los llama cuando se dispara algún evento en particular.

El Patrón Observador es un patrón de diseño que define una dependencia del tipo uno-a-muchos entre objetos, de manera que cuando uno de los objetos cambia su estado, notifica este cambio a todos los dependientes.

Gracias a ello, mediante el uso de Observadores (Escuchas de Eventos o Listeners), podemos realizar ciertas tareas en cada etapa de la ejecución de una petición en symfony.

El potencial que brinda el uso de este patrón, es muy amplio, ya que dá la posibilidad de extender de manera impresionante una funcionalidad, sin tener que modificar el código del proceso que dispara el evento, solo agregando escuchas como para hacer logs, calculos, enviar correos, he infinidad de cosas para un evento particular.

Instalación

Podemos instalar el componente de 2 formas:

- Usando el repositorio Git oficial:

  (https://github.com/symfony/EventDispatcher).

- Instalándolo vía Composer (symfony/event-dispatcher en Packagist).

Usando el Componente

Eventos:

Cuando un evento es enviado:

  • Nombres unicos.
  • Pueden ser escuchados por cualquier cantidad de listeners.
  • Se crea una instancia de Symfony\Component\EventDispatcher\Event y se pasa a todos los listeners.

Cuando se envía un evento, es identificado por un nombre único.

Se crea una instancia de la clase event del componente event dispatcher y se le pasa a todos los listeners

Convenciones de Nomenclatura

  • Utilizar sólo letras minúsculas, números, puntos y guiones bajos (_) (.).
  • Utilizar prefijos seguidos de un punto.
  • Terminar los nombres con un verbo que indique la acción que esta en curso.

Por ejemplo:

  • kernel.response
  • form.pre_set_data

Existen unos estandares a la hora de nombrar los eventos.

Siempre es una buena practica utilizar los estandares.

Clase EventDispatcher

Esta clase es la encargada de contener y llamar a los escuchas cuando se dispara o ejecuta un evento, es a traves de ella, que registraremos y eliminaremos las funciones o los metodos que estarán escuchando eventos, y es con esta misma clase que invocaremos la ejecución de cada evento.

Como podemos ver:

  • Primero instanciamos la clase eventDispatcher
  • Luego para registrar nuestro escucha o listener, creamos un objeto de nuestra clase
  • La registramos con el metodo addListener de la clase eventDispatcher, en donde le pasamos el nombre del evento que estara esperando escuchar y que metodo de nuestra clase se ejecutara cuando se dispare este evento.

Para disparar el evento se utiliza el metodo dispatch, pasando como parametro el nombre del evento.

<?php

use Symfony\Component\EventDispatcher\EventDispatcher;

$dispatcher = new EventDispatcher();
$listener = new MyListener();

$dispatcher->addListener('nombre_del_evento', array(
    $listener, 'miMetodoAction'
));

$dispatcher->dispatch('nombre_del_evento');

Deteniendo el flujo/propagación del evento

En algunos casos, puede ser necesario que un listener evite que se llame a otros listeners. Esto se puede lograr utilizando el metodo: stopPropagation()

<?php

use MyBundle\Event\MyEvent;

public function miMetodo(MyEvent $event)
{
    // ...

    $event->stopPropagation();

}

Ahora cualquier listerner de nombre_del_evento que no se haya llamado aún, no será invocado.

Es posible detectar si un evento fue detenido utilizando el método isPropagationStopped() que devuelve un valor booleano:

<?php

// ...

$dispatcher->dispatch('nombre_del_evento', $event);

if ($event->isPropagationStopped()) {

    // ...

}

Usando suscriptores de evento

  • Implementa la interfaz: Symfony\Component\EventDispatcher\EventSubscriberInterface
  • Requiere un solo método estático llamado: getSubscribedEvents.

Otra forma de escuchar eventos es a través de un suscriptor de eventos. Un suscriptor de eventos es una clase PHP que es capaz de decir al despachador exactamente a cuales eventos debe estar suscrito.

Se implementa una interfaz de la clase eventDispatcher, que requiere un solo metodo

Ejemplo:

<?php

namespace MyBundle\Event;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;

class myEventSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return array(
            'evento.a' => array('metodoX'),
            'evento.b' => array('metodoY'),
        );
    }

    public function metodoX(FilterResponseEvent $event)
    {
        // ...
    }

    public function metodoY(FilterResponseEvent $event)
    {
        // ...
    }
}

Para registrar un suscriptor al despachador, utiliza el método:

addSubscriber()

<?php

use MyBundle\Event\myEventSubscriber;

$subscriber = new myEventSubscriber();
$dispatcher->addSubscriber($subscriber);

}

El método getDispatcher

El EventDispatcher siempre inyecta una referencia a sí mismo en el objeto evento que se le pasa. Esto significa que todos los escuchas tienen acceso directo al objeto EventDispatcher a través del método getDispatcher() del objeto Event transmitido.

Esto es muy interesante ya que podemos despachar otro evento desde un listener

<?php

use Symfony\Component\EventDispatcher\Event;

class Foo
{
    public function myFooListener(Event $event)
    {
        $event->getDispatcher()->dispatch('nombre_evento', $event);

        // ...

    }


}

Inyectando el EventDispatcher en tus listeners.

  • Inyección en el constructor
  • Inyección en el definidor:

Si nuestra aplicación utiliza múltiples instancias del EventDispatcher, posiblemente tengamos que inyectar específicamente una instancia conocida del EventDispatcher en nuestros listeners. Esto se podría hacer inyectándolo en el constructor o con un definidor de la siguiente manera:

Inyección en el constructor:


use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class Foo
{
    protected $dispatcher = null;

    public function __construct(EventDispatcherInterface $dispatcher)
    {
        $this->dispatcher = $dispatcher;
    }
}

Inyección en el definidor:


use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class Foo
{
    protected $dispatcher = null;

    public function setEventDispatcher(EventDispatcherInterface $dispatcher)
    {
        $this->dispatcher = $dispatcher;
    }
}

Ejemplo Practico

Para explicar un poco mejor el uso de este componente, he tomado un ejemplo de nuestro compa#nero manuel.

La idea en el ejemplo es aprobar a ciertos usuarios en una aplicación luego de que estos se han registrado, además, al realizar la aprobación se quiere que le llegue un correo a dicho usuario informandole que su cuenta ha sido habilitada.

Clase User:

Comenzaremos con una clase que será nuestro modelo o entidad.

<?php

namespace MyBundle\Entity;

class User
{
    protected $name;
    protected $email;
    protected $status;

    // ...

}

Clase UserEvents:

Esta clase simplemente contendrá constantes que nos ayudarán a documentar los eventos y nos permitirán usar dichas constantes en vez de strings al despachar eventos, lo que ayuda a minimizar los errores al tipear.

<?php

namespace MyBundle\Entity;

final class UserEvents
{
    /**
    * Este evento se ejecuta antes de cambiar el estatus del usuario
    * a aprobado
    *
    * Los listener de este evento deben esperar una instancia de:
    *
    * Symfony\Component\EventDispatcher\GenericEvent
    *
    * Si alguno de los listener cancela la propagación del
    * evento (stopPropagation()), la aprobación
    * no se realiza, ni se llama al evento post_approve.
    */
    const PRE_APPROVE = 'my_bundle.user.pre_approve';

    /**
    * Este evento se ejecuta despues de cambiar el estatus del usuario
    * a aprobado
    *
    * Los listener de este evento deben esperar una instancia de:
    *
    * Symfony\Component\EventDispatcher\GenericEvent
    *
    */
    const POST_APPROVE = 'my_bundle.user.post_approve';

}

Clase UserManager:

Es una buena práctica crear un manager para nuestros modelos, y así no tener la lógica de los mismos directo en los controladores (recordemos: controladores flacos, modelos gordos).

Para efectos de este ejemplo, nuestro manager solo tendrá un método relevante para el manejo de los usuarios, el mismo tendrá por nombre approve y esperará una instancia de MyBundle\Entity\User que será el usuario que aprobaremos

<?php

namespace MyBundle\Model;

use MyBundle\UserEvents;
use MyBundle\Entity\User;
use Doctrine\ORM\EntityManager;
use Symfony\Component\EventDispatcher\GenericEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class UserManager
{

    protected $em;

    protected $dispatcher;

    public function __construct(
        EntityManager $em,
        EventDispatcherInterface $dispatcher
    ){
        $this->em = $em;
        $this->dispatcher = $dispatcher;
    }

    public function approve(User $user)
    {
        $event = new GenericEvent($user);
        $this->dispatcher->dispatch(UserEvents::PRE_APPROVE, $event);

        if ($event->isPropagationStopped()) {
            return false; //cancelamos la aprobación
        }

        $user->setStatus(User::STATUS_APPROVED);
        $this->em->persist($user);
        $this->em->flush();

        $event = new GenericEvent($user);
        $this->dispatcher->dispatch(UserEvents::POST_APPROVE, $event);

        return true;
    }

}

Registrando el UserManager en el Container

Nuestro UserManager necesita que se le pasen dos objetos para realizar sus tareas de aprobación, estos son el entity manager de doctrine y el event dispatcher de symfony, para hacer esto, registraremos nuestra clase como un servicio en el contenedor y le inyectamos los servicios/objetos que necesita:

#  MyBundle/Resources/config/services.yml

services:
    my_bundle.user_manager:
        class: MyBundle\Model\UserManager
        arguments:
            - @doctrine.orm.default_entity_manager
            - @event_dispatcher

El controlador

Ahora creamos nuestra acción en algún controlador, para que se realize el proceso de aprobación:

<?php

namespace MyBundle\Controller;

use MyBundle\Entity\User;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class UserController extends Controller
{

    /**

    * @ParamConverter("user", class="MyBundle:user") usamos anotaciones :)

    *

    * @link http://symfony.com/doc/master/bundles/SensioFrameworkExtraBundle/annotations/converters.html

    */

    public function approveAction(User $user)

    {

        if($this->get('my_bundle.user_manager')->approve($user)){
                // enviamos un flash por ejemplo
        }

        return $this->redirect(....);
    }
}

El Listener para el Correo

Notarán que el método approve de la clase UserManager no realiza el envío de correo al aprobar al usuario, esto es porque esta tarea se la dejaremos a un listener que crearemos a continuación.

<?php

namespace MyBundle\Listener;

use Symfony\Component\EventDispatcher\GenericEvent;

class SendApprovedEmailListener
{

    protected $mailer;


    public function __construct($mailer)

    {

        $this->mailer = $mailer;
    }

    /**
    * Este método será el encargado de enviar el correo electrónico luego de
    * que el usuario haya sido aprobado.
    *
    * Para más info sobre GenericEvent ver:
    * @link http://symfony.com/doc/current/components/event_dispatcher/generic_event.html
    */
    public function onPostApprove(GenericEvent $event)
    {
        $user = $event->getSubject(); //nos devuelve el objeto User

        $message = \Swift_Message::newInstance()
            ->setSubject('Cuenta Aprobada!')
            ->setFrom($from) //lo sacamos de algún lado (container, bd, ...)
            ->setTo($user->getEmail())
            ->setBody($body); //lo sacamos de algún lado (bd, twig, ...)

        $this->mailer->send($message);
    }
}

Ya tenemos nuestro listener creado, ahora debemos registrarlo en el contenedor y agregarle las etiquetas que lo identifiquen como un escucha de eventos:

#  MyBundle/Resources/config/services.yml
services:
	my_bundle.user_manager:
	....

my_bundle.listener.user.send_approved_email:
	class: MyBundle\Listener\SendApprovedEmailListener
	arguments:
		- @mailer
	tags:
		- {name: kernel.event_listener, event: my_bundle.user.post_approve, method: onPostApprove}

Gracias a la etiqueta kernel.event_listener de symfony, nuestra clase está escuchando el evento my_bundle.user.post_approve

Gracias !

Symfony The Event Dispatcher Component Francisco Silva - https://github.com/franjs Este componente permite implementar el Patrón Observador mediante el uso de la clase (EventDispatcher) que contiene a los escuchas de eventos y los llama cuando se dispara algún evento en particular. El Patrón Observador es un patrón de diseño que define una dependencia del tipo uno-a-muchos entre objetos, de manera que cuando uno de los objetos cambia su estado, notifica este cambio a todos los dependientes. Gracias a ello, mediante el uso de Observadores (Escuchas de Eventos o Listeners), podemos realizar ciertas tareas en cada etapa de la ejecución de una petición en symfony. El potencial que brinda el uso de este patrón, es muy amplio, ya que dá la posibilidad de extender de manera impresionante una funcionalidad, sin tener que modificar el código del proceso que dispara el evento, solo agregando escuchas como para hacer logs, calculos, enviar correos, he infinidad de cosas para un evento particular.