Eventos, jobs y transacciones en laravel

Spread the love

Una duda común, es como usar eventos y jobs con transacciones y creo que parte de esa duda radica en el hecho de que suelen ser un poco “mágicos” y no se ve que es lo que hacen a simple vista.

Así que determinar donde tienes que colocar una transacción, cuando usas eventos o jobs puede no ser sencillo la primeras veces. Sobre todo cuando no estamos acostumbrados a usar estas herramientas de Laravel.

Así que en el siguiente articulo vamos a ver los dos casos mas comunes.

Analizando el caso

Supongamos que tenemos este ejemplo sencillo.

$role = Role::findOrFail(1);
$user = User::create($request->all());
$role->users()->save($user);

Simplemente buscamos un role y lo asignamos a un nuevo usuario, vamos a suponer que el findOrFail(1) es para un role llamado “Básico” que debe de tener todo usuario que se crea la primera vez en el sistema.

Agregando una transacción

Bien, ahora vamos a suponer que el requerimiento de este sencillo código cambia y ahora te piden que el usuario no pueda ser creado si no se le asigna el role “Básico” que mencionamos.

Entonces rápidamente actualizas tu código para usar una transacción.

    try {
            DB::beginTransaction();

            $role = Role::findOrFail(1);
            $user = User::create($request->all());
            $role->users()->save($user);

            DB::commit();
        } catch (ModelNotFoundException $e) {
            DB::rollBack();
        }

De esta forma si algo falla el usuario no puede crearse sin un role.

si vemos esto en una imagen, nuestro código se puede ver como en la figura 1.

Imagina que el cuadro es la transacción que protege a las dos acciones que tiene nuestro código; que es crear el usuario y asignar el role. y ademas puedes observar que cada operación va en secuencia.

Muy bien, ahora supongamos que te piden por buenas practicas pasar la asignación del role a un evento, ya que es una consecuencia de crear un usuario.

Y es aquí donde llega la duda,

¿Dónde debe de estar la transacción?

Que te parece si analizamos este caso agregando un evento a nuestro ejemplo.

Evento con transacción.

como primer paso vas y registras el evento y su listener en el EventServiceProvider.

Registro de evento y listener

protected $listen = [
        Registered::class => [
            SendEmailVerificationNotification::class,
        ],
        //Registro de evento y listener.
        UserWasCreated::class => [
            AssignBasicRoleToUser::class,
        ],
    ];

Creación de evento

Después de esto, creamos el evento UserWasCreated para que acepte el usuario que vamos a crear.

class UserWasCreated
{
    use Dispatchable, InteractsWithSockets, SerializesModels;
    /**
     * @var User
     */
    public $user;

    /**
     * Create a new event instance.
     *
     * @param User $user
     */
    public function __construct(User $user)
    {
        $this->user = $user;
    }
}

Creación de listener

Posterior a esto, creamos un Listener (AssignBasicRoleToUser) que sera el encargado de asignar el role al usuario.

class AssignBasicRoleToUser
{
    /**
     * Handle the event.
     *
     * @param UserWasCreated $event
     * @return void
     * @throws \Exception
     */
    public function handle(UserWasCreated $event)
    {
        $role = Role::findOrFail(1);
        $role->users()->save($event->user);
    }
}

Actualizando nuestro código

Por ultimo actualizamos nuestro código original, para que dispare el evento después de crear el usuario.

   try {
            DB::beginTransaction();

            $user = User::create($event->data);
            event(UserWasCreated($user));

            DB::commit();
        } catch (ModelNotFoundException $e) {
            DB::rollBack();
        }

Si ejecutas esto vas a obtener el mismo resultado!.

¿Pero cómo es esto posible?

Para ver que sucede, representemos esto en una imagen.

Figura 2

Si observas el evento es como un “if” y lo que hace es que el código se ejecute en otro punto, pero todo se sigue ejecutando de forma secuencial y en la misma transacción .

así que para llegar al response, se tiene que ejecutar primero la creación del usuario, seguido del evento y el listener que asigna el role.

Por lo cual no es necesario hacer ningún cambio para usar transacciones con eventos.

Ahora, ¿Qué sucede si el evento se pasa a un job?

veamos este caso.

Evento como job y transacción.

Para hacer que un evento sea tratando de forma automática como un job, solo tenemos que implementar la interfaz ShouldQueue en el listener (AssignBasicRoleToUser).

También es importante agregar el trait InteractsWithQueue para controlar el job.

class AssignBasicRoleToUser implements ShouldQueue
{
    use InteractsWithQueue;

    /**
     * Handle the event.
     *
     * @param UserWasCreated $event
     * @return void
     * @throws \Exception
     */
    public function handle(UserWasCreated $event)
    {
            $role = Role::findOrFail(1);
            $role->users()->save($event->user);
    }
}

Bien, si ejecutas nuevamente el código y algo falla no funcionara la transacción!

Y esto se debe a que el código del listener se ejecuta en otro momento y en otro contexto. Es decir que si nuestro código esta en un controller, el listener se ejecuta en otro momento después de que el controller termina de enviar el response.

Esto lo podemos ver en la figura 3

Figura 3

Sí observas la acción para asignar el role esta en su propio hilo de ejecución por lo cual la transacción no puede usarse.

Para solucionar esto tenemos que mover la creación de usuario y la asignación del role dentro del listener y crear la transacción dentro de él, como se muestra en la figura 4.

Ahora nuestro código va a quedar como sigue.

Controlador

Si imaginamos que nuestro código esta en un controlador nuestro código queda como sigue.

event(new UserWasCreated($request->all()));

Observa que ahora al evento le pasamos solo los valores del request.

Evento UserWasCreated

class UserWasCreated
{
    use Dispatchable, InteractsWithSockets, SerializesModels;
 
    /**
     * @var array
     */
    public $data;

    /**
     * Create a new event instance.
     *
     * @param array $data
     */
    public function __construct(Array $data)
    {
        $this->data = $data;
    }
}

El evento ahora solo recibe un arreglo.

Listener AssignBasicRoleToUser

class AssignBasicRoleToUser implements ShouldQueue
{
    use InteractsWithQueue;

    /**
     * Handle the event.
     *
     * @param UserWasCreated $event
     * @return void
     * @throws \Exception
     */
    public function handle(UserWasCreated $event)
    {

        try {
            DB::beginTransaction();

            $role = Role::findOrFail(1);
            $user = User::create($event->data);
            $role->users()->save($user);

            DB::commit();
        } catch (ModelNotFoundException $e) {
            DB::rollBack();
        }
    }

    public function failed(UserWasCreated $event, $exception)
    {
        $this->delete();
    }

}

El cambio importante lo tenemos en el Listener, Ahora se hace cargo de todas las acciones y ademas le agregamos el método failed, con esto cuando ocurra una excepción no se creara el usuario y por lo tanto no se asignara el role, finalmente el job sera borrado de forma transparente.

Resumen

Hemos visto que los eventos en realidad se ejecutan en el mismo contexto que una transacción, por lo cual no es necesario realizar muchos cambios para utilizarlos.

En cambio cuando se usan eventos como jobs, estos se ejecutan fuera del ciclo normal de ejecución, por lo cual siempre es importante mover el código que esta dentro de la transacción dentro del Listener.

Por ultimo recuerda que si tienes alguna duda puedes dejar tu comentario.

Y no olvides compartir este articulo!