On Github franjs / event_dispatcher
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.
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).
Cuando un evento es enviado:
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
Por ejemplo:
Existen unos estandares a la hora de nombrar los eventos.
Siempre es una buena practica utilizar los estandares.
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:
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');
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()) { // ... }
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
<?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 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); // ... } }
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:
use Symfony\Component\EventDispatcher\EventDispatcherInterface; class Foo { protected $dispatcher = null; public function __construct(EventDispatcherInterface $dispatcher) { $this->dispatcher = $dispatcher; } }
use Symfony\Component\EventDispatcher\EventDispatcherInterface; class Foo { protected $dispatcher = null; public function setEventDispatcher(EventDispatcherInterface $dispatcher) { $this->dispatcher = $dispatcher; } }
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.
Comenzaremos con una clase que será nuestro modelo o entidad.
<?php namespace MyBundle\Entity; class User { protected $name; protected $email; protected $status; // ... }
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'; }
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; } }
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
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(....); } }
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