Usar acciones en Laravel con Laravel Actions

Breakdown de uno de mis paquetes favoritos en Laravel

Usar acciones en Laravel con Laravel Actions
Photo by Xavi Cabrera / Unsplash

Uno de mis paquetes favoritos, y que usé en mi primer directo, es Laravel Actions.

Qué es una acción en Laravel

El concepto de acción es muy conocido en el mundo de Laravel. A grandes rasgos, viene a ser una clase que hace una única cosa. Es decir, en lugar de tener el típico controlador ProductsController con los métodos index, show, update, store y destroy; crearíamos 5 controladores diferentes:

  • Products/ListProductsController
  • Products/ShowProductController
  • Products/UpdateProductController
  • Products/StoreProductController
  • Products/DestroyProductController

Esto está además escrito en la documentación de Laravel como Single Action Controllers.

Por ejemplo, si estuviéramos usando los SAC nuestro código para listar los productos se quedaría algo así:

// app/Http/Controllers/Products/ListProductsController

<?php
 
namespace App\Http\Controllers\Products;
 
use App\Models\Product;
 
class ListProductsController extends Controller
{
    public function __invoke()
    {
        return response()->json([
        	'products' => Product::orderBy('name')->paginate(15),
        ]);
    }
}

// api.php

Route::get('products', ListProductsController::class);

Si esto ya está soportado en Laravel, ¿por qué necesitamos una librería?

Qué es Laravel Actions

Pues bien, Laravel Actions no deja de ser un wrapper alrededor de los Single Action Controllers (SAC), añadiéndoles más funcionalidades bastante útiles. Nos permite escribir código más atómico en nuestra aplicación, que será después reutilizable desde muchas partes. La misma action puede usarse desde otra parte del código, como Job, como Command, como Listener o como Controller.

Imaginemos que tenemos una acción para enviar un e-mail a un usuario tras un pedido. Nuestro Laravel Action sería algo así:

<?php

namespace App\Actions\Orders;

use App\Models\Order;
use Lorisleiva\Actions\Concerns\AsAction;
// ...

class NotifyOrderToUser
{
    use AsAction;

    public function handle(Order $order)
    {
    	Mail::to($order->user->email)->send(new OrderNotificationMail($order));
        // send a copy to the admin
        Mail::to(config('myapp.emails.admin'))->send(new OrderNotificationMail($order));
    }
}

Desde cualquier otra parte de nuestro código, podríamos ejecutar lo siguiente:

<?php
 
namespace App\Http\Controllers\Products;
 
use App\Models\Product;
use App\Actions\Orders\NotifyOrderToUser;
 
class OrderController extends Controller
{
    public function store()
    {
    	// ...
        // process
        // ...
        
        NotifyOrderToUser::run($order);

        // ...
    }
}

Ejecutar el ::run() ejecutaría el código del método handle() de forma síncrona. Si queremos usar esa misma acción y mandarla a la cola de procesos para ejecutarlo de forma asíncrona solamente hay que cambiar el ::run a ::dispatch:

<?php
 
namespace App\Http\Controllers\Products;
 
use App\Models\Product;
use App\Actions\Orders\NotifyOrderToUser;
 
class OrderController extends Controller
{
    public function store()
    {
    	// ...
        // process
        // ...
        
        NotifyOrderToUser::dispatch($order);

        // ...
    }
}

Vamos a por otra ventaja: Si quisiéramos poder usar este action también desde la línea de comandos para poder re-enviar el e-mail manualmente (previo registro de los comandos de las actions):

<?php

namespace App\Actions\Orders;

use App\Models\Order;
use Illuminate\Console\Command;
use Lorisleiva\Actions\Concerns\AsAction;
// ...

class NotifyOrderToUser
{
    use AsAction;
    
    public string $commandSignature = 'order:notify {orderId}';

    public function handle(Order $order)
    {
    	Mail::to($order->user->email)->send(new OrderNotificationMail($order));
        // send a copy to the admin
        Mail::to(config('myapp.emails.admin'))->send(new OrderNotificationMail($order));
    }
    
    public function asCommand(Command $command)
    {
    	$this->handle(Order::findOrFail($command->argument('orderId')));
        
        $command->info('Sent!');
    }
}

Ahora, la misma acción podemos ejecutarla desde cualquier parte del código ty también desde artisan de la siguiente forma:

php artisan order:notify 1

También podemos usar los métodos asController, asJob y asListener para ejecutar la acción en cualquiera de estas situaciones.

Además, tiene una funcionalidad de mocking muy útil a la hora de hacer tests.

Gracias a esta librería estamos produciendo un código mucho más modular y testeable que podemos reutilizar en diferentes proyectos en nuestra empresa 🎉