Este foro ya no está activo, así que no puedes publicar nuevas preguntas ni responder a las preguntas existentes.

Evento kernel.terminate de Symfony

1 de julio de 2015

Buenas, Estoy haciendo un ejemplo de eventos, en concreto utilizando kernel.terminate.

La idea es que cuando un controlador concreto envíe la respuesta al usuario, internamente comience a crear una determinada acción, por ejemplo, el envío de un email o generar un pdf o algo más pesado y evitar la espera en la respuesta al usuario.

Estoy probando con este código:

1) Creo el servicio:

after_response.subscriber:
    class: AppBundle\EventListener\AfterResponseSubscriber
    tags:
        - { name: kernel.event_subscriber }

2) Creo el listener mediante un subscriber:

<?php
 
namespace AppBundle\EventListener;
 
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
class AfterResponseSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return array(
            'kernel.terminate' => 'sendEmail'
        );
    }
 
    public function sendEmail()
    {
        echo "Prueba";
    }
}

Un problema que veo es que cuando voy a la ruta homepage / del controlador indexAction(). En la respuesta me repite dos veces al final de todo la palabra "Prueba". ¿Por qué dos veces y no solo una?

Otra cuestión es cómo podría filtrar por controladores para que solo se ejecute esa función sendEmail() del evento EventSubscriberInterface en un solo controlador y no todos, en este caso el indexAction().

Muchas gracias de antemano.


Respuestas

#1

Buenas Juan, con respecto al primer punto en donde dices que la respuesta aparece dos veces, es muy probable que la segunda vez que se ejecuta, es en el llamado ajax que symfony hace para cargar y mostrar el toolbar del profiler. Lo que quiere decir que cuando estés en producción, o cuando desabilites el toolbar en desarrollo, tu mensajé se mostrará solo una vez.

Por otro lado, la idea de registrar listeners en el event_dispatcher de symfony creando servicios y etiquetandolos como listeners, es para cuando cierto proceso necesita ser ejecutado en la gran mayoría de las peticiones (en algunos casos, en todas las peticiones).

Como en tu caso solo necesitas que el listener se ejecute en un controlador, puedes hacer algo como lo siguiente:

after_response.subscriber:
    class: AppBundle\EventListener\AfterResponseSubscriber

Sigues registrando el listener como servicio, pero sin etiquetarlo como tal.

public function indexAction()
{
   //.. realizas tu proceso
 
   // en la acción del controlador que necesitas que ejecute el proceso en el kernel.terminate
   // añades el listener al event_dispatcher de symfony
   $listener = $this->get('after_response.subscriber');
   $this->get('event_dispatcher')->addEventSubscriber($listener);
 
   $this->get('event_dispatcher')->addEventListener('kernel.terminate', function(){
      echo "Otra forma de hacerlo sin usar una clase listener";
   });
 
   // añadiendolo como un listener de un evento especifico y no como un subscriber de eventos.
   $this->get('event_dispatcher')->addEventListener('kernel.terminate', [$listener, 'sendEmail']);
 
   return $this->render(...); // retornas tu respuesta
   // o return new Response(...);
}

En el ejemplo se ven varias formas en las que puedes registrar un listener para el evento kernel.terminate que solo se está registrando para una acción especifica y no para todas las acciones de la aplicación.

Saludos!!!

@manuel_j555

2 julio 2015, 5:23
#2

Muchas gracias Manuel.

Ahora llamando al event_dispatcher desde el controlador sale solo una vez y funciona solo en este controlador.

/**
* @Route("/", name="homepage")
*/
public function indexAction()
{
  $listener = $this->get('after_response.subscriber');
  $this->get('event_dispatcher')->addSubscriber($listener);
 
  return $this->render('default/index.html.twig');
}

Para adelgazar el controlador añadiré el event_subscriber en el servicio de enviar email. Creo que sería la mejor opción.

Muchas gracias por su respuesta Saludos

@JuanluGarciaB

2 julio 2015, 8:42
#3

Creo que el problema de base que te está sucediendo es que usas un evento demasiado genérico. Como tú mismo dices, el problema a resolver es: "cuando se ejecute un determinado controlador, quiero enviar un email al final". Sin embargo, el eevnto que estás usando es: "siempre que se acabe una petición, ejecuta esto".

Por eso creo que deberías olvidarte del evento kernel.terminate y usar tu propio evento, al que además puedes dar un nombre semántico (por ejemplo: usuario_registrado, registro_finalizado, etc.).

@javiereguiluz

2 julio 2015, 9:00
#4

Entonces, si me defino mi propio evento, me ahorro de añadir las llamadas al servicio event_dispatcher en el controlador.

Una duda que me surge con todo esto y que he estado haciendo mal o menos óptimo es el típico evento name: doctrine.event_listener, event: prePersist este evento se ejecuta cada vez que hay un persist en TODAS las entidades y en el listener siempre pongo

public function prePersist(LifecycleEventArgs $args)
    {
        $entity = $args->getEntity();
        if($entity instanceof Entity){
            //realizo acción
        }
    }

De esta forma consume más recursos debido a que tiene que ejecutarse en todas las clases. La manera óptima sería creando tu propio evento (por ejemplo: usuario_nuevo).

Según he mirado en symfony.com para crear tu propio evento sería siguiendo estos pasos

Muchas gracias Javier por su paciencia e interés

@JuanluGarciaB

2 julio 2015, 13:20
#5

Tienes razón sobre lo de Doctrine. No obstante, el problema no es tan importante como usar el evento kernel.terminate. La diferencia es que el evento perPersist() sólo se ejecuta cuando se persiste una entidad (que no es algo que se hace continuamente en las aplicaciones), mientras que el evento kernel.terminate se ejecuta en todas las peticiones de tu aplicación, por lo que sí va a afectar al rendimiento.

La forma más sencilla de hacer tu propio evento consiste en usar el objeto GenericEvent y ejecutar el método dispatch() con el nombre del evento para que luego lo escuche algún listener o susbcriber.

Si quieres hacerlo un poco mejor, define una clase como esta para centralizar el nombre de los eventos propios de tu aplicación y así poder usarlos mediante las constantes de esa clase. Esto no es obligatorio, pero es "más profesional".

Respecto al GenericEvent, te aconsejo que lo uses a menos que tu aplicación sea muy compleja. El motivo es que funciona bien como objeto genérico para transportar información y así te ahorras tener que definir tu propia clase. Yo lo uso por ejemplo en este método dispatch() de un bundle que he publicado.

@javiereguiluz

2 julio 2015, 13:34
#6

De todas formas creo que el crear un evento propio (cosa en la que estoy muy de acuerdo) no resuelve el problema de que ese evento sea ejecutado en el kernel.terminate o luego de devolver la respuesta al cliente.

@manuel_j555

2 julio 2015, 14:50
#7

@manuel_j555 tienes razón. La idea sería dejar de utilizar kernel.terminate y sólo lanzar el evento propio donde lo necesites. Respecto a ejecutar tareas pesadas después de responder a la petición, si de verdad son tan complejas, es mejor no hacerlas en el controlador.

La solución obiva, aunque costosa desde el punto de vista de la administración de sistemas, sería usar un sistema de colas tipo RabbitMQ. La solución pragmática sería guardar en la base de datos una columna especial para estas cosas.

Imagina que quieres hacer algo después de que se registre un usuario. Sólo tienes que definir una columna estado para los usuarios. Cuando se registra, cambias su valor a registrado. Luego una tarea programada busca esos usuarios y hace la tarea pesada (por ejemplo enviar el email). Entones se cambia el estado del usuario a notificado. Cuando el usuario pincha en el email de confirmación, se cambia el estado a confirmado, etc.

@javiereguiluz

2 julio 2015, 14:59