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:
Qué es el Patrón Action y sus ventajas
Cómo organizar tu código
Cómo inyectar tus acciones con el contenedor de Laravel
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.phpEste 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 hacernew CreateUserAction()ni usarapp(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 --unitCrea un archivo en
tests/Unit/.Test de feature:
php artisan make:test UserControllerTestCrea 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 --unitAhora 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:
RefreshDatabasereinicia 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 UserControllerTestEsto 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:
Feature Test: Se enfoca en probar el flujo completo de la aplicación (petición HTTP, respuesta, DB).
postJson: Enviamos una peticiónPOSTcon contenido JSON a la ruta/users.assertJson: Verificamos la estructura y contenido del JSON de respuesta.assertDatabaseHas: Revisamos que exista un registro específico en la tablausers.
Nota: Asegúrate de tener definidas las rutas para tus métodos en
routes/web.phporoutes/api.php. Un ejemplo sencillo podría ser:Route::post('/users', [UserController::class, 'store']);
6. Buenas Prácticas y Consejos
Responsabilidad Única
Asegúrate de que cada Action cumpla un propósito claro: “CreateUserAction”, “SendEmailAction”, etc.Refactoriza
Si tu Action crece demasiado, considera separarla en múltiples Actions.Inyección de Dependencias
Utiliza el contenedor de Laravel para resolver tus dependencias. Esto facilita las pruebas y el mantenimiento.Nombra adecuadamente
Usa nombres claros y significativos para tus tests y tus métodos de prueba.Rutinas de Limpieza
UtilizaRefreshDatabase(oDatabaseTransactions) 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!
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.
star Incluido en cualquier suscripción