Saltar al contenido

En el desarrollo con Laravel, tarde o temprano los controladores comienzan a crecer y a llenarse de lógica que no pertenece necesariamente allí. El Patrón Action (Action Pattern) ofrece una solución al dividir la lógica en clases que cumplen una única función: crear, actualizar, eliminar, procesar pagos, etc.

En esta guía, aprenderás:

  1. Qué es el Patrón Action y sus ventajas

  2. Cómo organizar tu código

  3. Cómo inyectar tus acciones con el contenedor de Laravel

  4. Cómo escribir tests para tus acciones


1. ¿Qué es el Patrón Action?

El Patrón Action promueve la creación de clases enfocadas en una sola responsabilidad. Cada clase se encarga de llevar a cabo una acción específica:

  • Crear un usuario

  • Actualizar un usuario

  • Eliminar un usuario

  • Generar reportes

  • Enviar correos

  • y más…

Ventajas

  • Responsabilidad Única: Código más legible y mantenible.

  • Reutilización: Fácil de usar la misma lógica en distintas partes de la aplicación.

  • Facilidad de Testeo: Clases más pequeñas y enfocadas en lo que deben hacer.

  • Escalabilidad: El crecimiento de la aplicación es más ordenado y modular.


2. Estructura de Carpetas

Por convención, podrías crear un directorio Actions dentro de app. Por ejemplo:

app
└── Actions
    ├── CreateUserAction.php
    ├── UpdateUserAction.php
    └── DeleteUserAction.php

Este patrón no es una regla obligatoria, pero ayuda a mantener una estructura clara y ordenada.


3. Ejemplos de Acciones

3.1 CreateUserAction

<?php

namespace App\Actions;

use App\Models\User;
use Illuminate\Support\Facades\Hash;

class CreateUserAction
{
    /**
     * Invoca la acción de crear un usuario.
     *
     * @param  array  $data
     * @return User
     */
    public function __invoke(array $data): User
    {
        return User::create([
            'name'     => $data['name'],
            'email'    => $data['email'],
            'password' => Hash::make($data['password']),
        ]);
    }
}

La lógica para crear un usuario está contenida aquí. Se aprovecha la función __invoke() para que la clase sea invocable como si fuera un método.

3.2 UpdateUserAction

<?php

namespace App\Actions;

use App\Models\User;
use Illuminate\Support\Facades\Hash;

class UpdateUserAction
{
    /**
     * Actualiza la información de un usuario.
     *
     * @param  User   $user
     * @param  array  $data
     * @return User
     */
    public function __invoke(User $user, array $data): User
    {
        if (isset($data['password'])) {
            $data['password'] = Hash::make($data['password']);
        }

        $user->update($data);

        return $user;
    }
}

3.3 DeleteUserAction

<?php

namespace App\Actions;

use App\Models\User;

class DeleteUserAction
{
    /**
     * Elimina un usuario y devuelve verdadero o falso.
     *
     * @param  User  $user
     * @return bool
     */
    public function __invoke(User $user): bool
    {
        return $user->delete();
    }
}

4. Inyección de Dependencias con el Contenedor IoC

En lugar de instanciar manualmente las clases con new, utilizaremos el contenedor de Laravel para que se encargue de resolver nuestras acciones. Esto facilita la inyección de dependencias y promueve el desacoplamiento.

Ejemplo de Controlador: UserController

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Http\Requests\UserRequest;
use App\Models\User;

// Importamos las acciones
use App\Actions\CreateUserAction;
use App\Actions\UpdateUserAction;
use App\Actions\DeleteUserAction;

class UserController extends Controller
{
    /**
     * Almacena un nuevo usuario.
     */
    public function store(UserRequest $request, CreateUserAction $createUserAction)
    {
        $data = $request->validated();

        // Laravel inyecta la clase CreateUserAction a través de su contenedor IoC
        $user = $createUserAction($data);

        return response()->json([
            'message' => 'Usuario creado exitosamente',
            'user'    => $user,
        ]);
    }

    /**
     * Actualiza un usuario existente.
     */
    public function update(UserRequest $request, User $user, UpdateUserAction $updateUserAction)
    {
        $data = $request->validated();

        // El contenedor inyecta la clase UpdateUserAction
        $updatedUser = $updateUserAction($user, $data);

        return response()->json([
            'message' => 'Usuario actualizado',
            'user'    => $updatedUser,
        ]);
    }

    /**
     * Elimina un usuario.
     */
    public function destroy(User $user, DeleteUserAction $deleteUserAction)
    {
        $deleted = $deleteUserAction($user);

        return response()->json([
            'message' => $deleted ? 'Usuario eliminado' : 'No se pudo eliminar',
        ]);
    }
}

Nota:
De esta forma, no necesitas hacer new CreateUserAction() ni usar app(CreateUserAction::class). Laravel inyectará automáticamente la acción apropiada, siempre y cuando hayas definido los tipos de parámetros en la función.


5. Tests con PHPUnit

A continuación, veremos cómo crear y ejecutar tests unitarios (para acciones) y tests de tipo feature (para el controlador).

5.1 Comandos para Crear Tests

  • Test de unidad:

    php artisan make:test CreateUserActionTest --unit

    Crea un archivo en tests/Unit/.

  • Test de feature:

    php artisan make:test UserControllerTest

    Crea un archivo en tests/Feature/.

Tip: Laravel 11 ya trae PHPUnit integrado. No necesitas instalarlo aparte.

5.2 Test Unitario de una Acción

Imagina que acabamos de ejecutar:

php artisan make:test CreateUserActionTest --unit

Ahora tenemos tests/Unit/CreateUserActionTest.php. En él, puedes escribir:

<?php

namespace Tests\Unit\Actions;

use Tests\TestCase;
use App\Models\User;
use App\Actions\CreateUserAction;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;

class CreateUserActionTest extends TestCase
{
    use RefreshDatabase;

    /**
     * Prueba que la acción CreateUserAction cree un usuario correctamente.
     */
    public function test_create_user_action_creates_new_user(): void
    {
        // Verificamos que la base de datos inicia sin usuarios
        $this->assertDatabaseCount('users', 0);

        // Resolvemos la acción utilizando el contenedor de Laravel
        $action = app(CreateUserAction::class);

        // Datos simulados (podrías usar una factory también)
        $data = [
            'name' => 'John Doe',
            'email' => '[email protected]',
            'password' => 'secret',
        ];

        // Ejecutamos la acción
        $user = $action($data);

        // Verificamos la creación exitosa
        $this->assertInstanceOf(User::class, $user);
        $this->assertEquals('John Doe', $user->name);
        $this->assertEquals('[email protected]', $user->email);
        // Confirmamos que la contraseña esté hasheada
        $this->assertTrue(Hash::check('secret', $user->password));

        // Verificamos que ahora exista 1 usuario en la base de datos
        $this->assertDatabaseCount('users', 1);
    }
}

Explicación:

  • RefreshDatabase reinicia la base de datos antes de cada test, asegurando un entorno limpio.

  • app(CreateUserAction::class): Resolvemos la acción desde el contenedor.

  • Afirmaciones: Comprobamos que la base de datos y los valores del usuario sean los esperados.

5.3 Test de Feature para el Controlador

Ahora creamos una prueba de tipo feature con:

php artisan make:test UserControllerTest

Esto generará tests/Feature/UserControllerTest.php. En él, podemos escribir:

<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;

class UserControllerTest extends TestCase
{
    use RefreshDatabase;

    /**
     * Prueba la ruta de creación de usuario (store) a través del controlador.
     */
    public function test_can_create_user_via_controller(): void
    {
        // Verificamos que no haya usuarios al inicio
        $this->assertDatabaseCount('users', 0);

        // Hacemos una petición POST a la ruta (asegúrate de definirla en web.php o api.php)
        $response = $this->postJson('/users', [
            'name' => 'John Doe',
            'email' => '[email protected]',
            'password' => 'secret',
        ]);

        // Verificamos el estado HTTP y la respuesta JSON
        $response->assertStatus(201)
                 ->assertJson([
                     'message' => 'Usuario creado exitosamente',
                 ]);

        // Verificamos que efectivamente se ha creado un usuario en la BD
        $this->assertDatabaseCount('users', 1);
        $this->assertDatabaseHas('users', [
            'email' => '[email protected]',
        ]);
    }
}

Explicación:

  1. Feature Test: Se enfoca en probar el flujo completo de la aplicación (petición HTTP, respuesta, DB).

  2. postJson: Enviamos una petición POST con contenido JSON a la ruta /users.

  3. assertJson: Verificamos la estructura y contenido del JSON de respuesta.

  4. assertDatabaseHas: Revisamos que exista un registro específico en la tabla users.

Nota: Asegúrate de tener definidas las rutas para tus métodos en routes/web.php o routes/api.php. Un ejemplo sencillo podría ser:

Route::post('/users', [UserController::class, 'store']);

6. Buenas Prácticas y Consejos

  1. Responsabilidad Única
    Asegúrate de que cada Action cumpla un propósito claro: “CreateUserAction”, “SendEmailAction”, etc.

  2. Refactoriza
    Si tu Action crece demasiado, considera separarla en múltiples Actions.

  3. Inyección de Dependencias
    Utiliza el contenedor de Laravel para resolver tus dependencias. Esto facilita las pruebas y el mantenimiento.

  4. Nombra adecuadamente
    Usa nombres claros y significativos para tus tests y tus métodos de prueba.

  5. Rutinas de Limpieza
    Utiliza RefreshDatabase (o DatabaseTransactions) para mantener tus pruebas aisladas y reproducibles.


Conclusión

El Patrón Action en Laravel te ayuda a mantener tus controladores livianos y ordenados, relegando la lógica de negocio a clases pequeñas y enfocadas. Al inyectar esas acciones mediante el contenedor IoC de Laravel, tu código se vuelve más desacoplado, propicio para la reutilización y el testeo.

Además, gracias a PHPUnit integrado, puedes cubrir tu aplicación con tests unitarios (para las acciones) y tests de tipo feature (para el controlador y la interacción con las rutas). Con estas prácticas, tu base de código se mantendrá escalable, segura y fácil de mantener a medida que tu proyecto crezca.

📚 ¿Quieres profundizar más?

He creado un libro completo sobre este tema: Acciones en Laravel, donde encontrarás ejemplos adicionales, casos de uso avanzados y mejores prácticas.

¡Ahora que tienes esta guía, estás listo para refactorizar tus controladores, escribir código más limpio y robusto, y cubrir tu aplicación con un set completo de pruebas!

¿Quieres seguir profundizando en las últimas novedades de Laravel?

No te pierdas nuestra ruta de Laravel 11, donde exploramos las funciones más recientes del framework, prácticas avanzadas y consejos para optimizar tus proyectos. ¡Te esperamos allí para que lleves tus aplicaciones al siguiente nivel!

school Curso completo

Curso Laravel 12
Completo 2026

El único curso 100% actualizado que incluye Laravel 12, Livewire 3, Vue 3, React 19 e Inertia 2. Aprende con proyectos reales y las últimas funcionalidades.

access_time 8 horas de contenido
layers 4 tecnologías en 1
update 100% actualizado
code Proyectos prácticos
Ver Curso Laravel 12 arrow_forward

star Incluido en cualquier suscripción

Rutas de aprendizaje