Patrones de arquitectura Laravel, routing/controladores, Eloquent ORM, capas de servicio, colas, eventos, caché y API resources para aplicaciones en producción.
Scanned 9/4/2026
Install to Claude Code
npx -y skills add affaan-m/everything-claude-code --skill laravel-patterns --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Laravel Patterns?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/affaan-m-laravel-patterns-ecc)More formats (shields.io, HTML) on the badges page.
---
name: laravel-patterns
description: Patrones de arquitectura Laravel, routing/controladores, Eloquent ORM, capas de servicio, colas, eventos, caché y API resources para aplicaciones en producción.
origin: ECC
---
# Patrones de Desarrollo Laravel
Patrones de arquitectura Laravel de nivel producción para aplicaciones escalables y mantenibles.
## Cuándo Usar
- Construir aplicaciones web o APIs con Laravel
- Estructurar controladores, servicios y lógica de dominio
- Trabajar con modelos Eloquent y relaciones
- Diseñar APIs con resources y paginación
- Agregar colas, eventos, caché y jobs en segundo plano
## Cómo Funciona
- Estructurar la app con límites claros (controladores -> servicios/actions -> modelos).
- Usar bindings explícitos y bindings con scope para mantener el routing predecible; aplicar autorización para el control de acceso.
- Favorecer modelos tipados, casts y scopes para mantener la lógica de dominio consistente.
- Mantener el trabajo intensivo de IO en colas y cachear lecturas costosas.
- Centralizar la configuración en `config/*` y mantener los entornos explícitos.
## Ejemplos
### Estructura del Proyecto
Usar un layout convencional de Laravel con límites de capa claros (HTTP, servicios/actions, modelos).
### Layout Recomendado
```
app/
├── Actions/ # Casos de uso de un solo propósito
├── Console/
├── Events/
├── Exceptions/
├── Http/
│ ├── Controllers/
│ ├── Middleware/
│ ├── Requests/ # Validación con Form Requests
│ └── Resources/ # API resources
├── Jobs/
├── Models/
├── Policies/
├── Providers/
├── Services/ # Servicios de dominio coordinadores
└── Support/
config/
database/
├── factories/
├── migrations/
└── seeders/
resources/
├── views/
└── lang/
routes/
├── api.php
├── web.php
└── console.php
```
### Controladores -> Servicios -> Actions
Mantener los controladores delgados. Poner la orquestación en servicios y la lógica de un solo propósito en actions.
```php
final class CreateOrderAction
{
public function __construct(private OrderRepository $orders) {}
public function handle(CreateOrderData $data): Order
{
return $this->orders->create($data);
}
}
final class OrdersController extends Controller
{
public function __construct(private CreateOrderAction $createOrder) {}
public function store(StoreOrderRequest $request): JsonResponse
{
$order = $this->createOrder->handle($request->toDto());
return response()->json([
'success' => true,
'data' => OrderResource::make($order),
'error' => null,
'meta' => null,
], 201);
}
}
```
### Routing y Controladores
Preferir route-model binding y controladores de recursos para mayor claridad.
```php
use Illuminate\Support\Facades\Route;
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('projects', ProjectController::class);
});
```
### Route Model Binding con Scope
Usar bindings con scope para prevenir acceso entre tenants.
```php
Route::scopeBindings()->group(function () {
Route::get('/accounts/{account}/projects/{project}', [ProjectController::class, 'show']);
});
```
### Rutas Anidadas y Nombres de Binding
- Mantener prefijos y rutas consistentes para evitar doble anidamiento (ej. `conversation` vs `conversations`).
- Usar un único nombre de parámetro que coincida con el modelo vinculado (ej. `{conversation}` para `Conversation`).
- Preferir bindings con scope al anidar para aplicar relaciones padre-hijo.
```php
use App\Http\Controllers\Api\ConversationController;
use App\Http\Controllers\Api\MessageController;
use Illuminate\Support\Facades\Route;
Route::middleware('auth:sanctum')->prefix('conversations')->group(function () {
Route::post('/', [ConversationController::class, 'store'])->name('conversations.store');
Route::scopeBindings()->group(function () {
Route::get('/{conversation}', [ConversationController::class, 'show'])
->name('conversations.show');
Route::post('/{conversation}/messages', [MessageController::class, 'store'])
->name('conversation-messages.store');
Route::get('/{conversation}/messages/{message}', [MessageController::class, 'show'])
->name('conversation-messages.show');
});
});
```
Si deseas que un parámetro resuelva a una clase de modelo diferente, definir un binding explícito. Para lógica de binding personalizada, usar `Route::bind()` o implementar `resolveRouteBinding()` en el modelo.
```php
use App\Models\AiConversation;
use Illuminate\Support\Facades\Route;
Route::model('conversation', AiConversation::class);
```
### Bindings del Contenedor de Servicios
Vincular interfaces a implementaciones en un service provider para una inyección de dependencias clara.
```php
use App\Repositories\EloquentOrderRepository;
use App\Repositories\OrderRepository;
use Illuminate\Support\ServiceProvider;
final class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(OrderRepository::class, EloquentOrderRepository::class);
}
}
```
### Patrones de Modelos Eloquent
### Configuración del Modelo
```php
final class Project extends Model
{
use HasFactory;
protected $fillable = ['name', 'owner_id', 'status'];
protected $casts = [
'status' => ProjectStatus::class,
'archived_at' => 'datetime',
];
public function owner(): BelongsTo
{
return $this->belongsTo(User::class, 'owner_id');
}
public function scopeActive(Builder $query): Builder
{
return $query->whereNull('archived_at');
}
}
```
### Casts Personalizados y Objetos de Valor
Usar enums u objetos de valor para tipado estricto.
```php
use Illuminate\Database\Eloquent\Casts\Attribute;
protected $casts = [
'status' => ProjectStatus::class,
];
```
```php
protected function budgetCents(): Attribute
{
return Attribute::make(
get: fn (int $value) => Money::fromCents($value),
set: fn (Money $money) => $money->toCents(),
);
}
```
### Eager Loading para Evitar N+1
```php
$orders = Order::query()
->with(['customer', 'items.product'])
->latest()
->paginate(25);
```
### Query Objects para Filtros Complejos
```php
final class ProjectQuery
{
public function __construct(private Builder $query) {}
public function ownedBy(int $userId): self
{
$query = clone $this->query;
return new self($query->where('owner_id', $userId));
}
public function active(): self
{
$query = clone $this->query;
return new self($query->whereNull('archived_at'));
}
public function builder(): Builder
{
return $this->query;
}
}
```
### Global Scopes y Soft Deletes
Usar global scopes para filtrado por defecto y `SoftDeletes` para registros recuperables.
Usar ya sea un global scope o un named scope para el mismo filtro, no ambos, a menos que se desee comportamiento en capas.
```php
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Builder;
final class Project extends Model
{
use SoftDeletes;
protected static function booted(): void
{
static::addGlobalScope('active', function (Builder $builder): void {
$builder->whereNull('archived_at');
});
}
}
```
### Query Scopes para Filtros Reutilizables
```php
use Illuminate\Database\Eloquent\Builder;
final class Project extends Model
{
public function scopeOwnedBy(Builder $query, int $userId): Builder
{
return $query->where('owner_id', $userId);
}
}
// En servicio, repositorio, etc.
$projects = Project::ownedBy($user->id)->get();
```
### Transacciones para Actualizaciones Multi-Paso
```php
use Illuminate\Support\Facades\DB;
DB::transaction(function (): void {
$order->update(['status' => 'paid']);
$order->items()->update(['paid_at' => now()]);
});
```
### Migraciones
### Convención de Nomenclatura
- Los nombres de archivo usan timestamps: `YYYY_MM_DD_HHMMSS_create_users_table.php`
- Las migraciones usan clases anónimas (sin clase con nombre); el nombre del archivo comunica la intención
- Los nombres de tablas son `snake_case` y plurales por defecto
### Ejemplo de Migración
```php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('orders', function (Blueprint $table): void {
$table->id();
$table->foreignId('customer_id')->constrained()->cascadeOnDelete();
$table->string('status', 32)->index();
$table->unsignedInteger('total_cents');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('orders');
}
};
```
### Form Requests y Validación
Mantener la validación en Form Requests y transformar las entradas a DTOs.
```php
use App\Models\Order;
final class StoreOrderRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()?->can('create', Order::class) ?? false;
}
public function rules(): array
{
return [
'customer_id' => ['required', 'integer', 'exists:customers,id'],
'items' => ['required', 'array', 'min:1'],
'items.*.sku' => ['required', 'string'],
'items.*.quantity' => ['required', 'integer', 'min:1'],
];
}
public function toDto(): CreateOrderData
{
return new CreateOrderData(
customerId: (int) $this->validated('customer_id'),
items: $this->validated('items'),
);
}
}
```
### API Resources
Mantener respuestas de API consistentes con resources y paginación.
```php
$projects = Project::query()->active()->paginate(25);
return response()->json([
'success' => true,
'data' => ProjectResource::collection($projects->items()),
'error' => null,
'meta' => [
'page' => $projects->currentPage(),
'per_page' => $projects->perPage(),
'total' => $projects->total(),
],
]);
```
### Eventos, Jobs y Colas
- Emitir eventos de dominio para efectos secundarios (emails, analíticas)
- Usar jobs en cola para trabajo lento (reportes, exportaciones, webhooks)
- Preferir handlers idempotentes con reintentos y backoff
### Caché
- Cachear endpoints y consultas costosas con muchas lecturas
- Invalidar cachés en eventos del modelo (created/updated/deleted)
- Usar tags al cachear datos relacionados para facilitar la invalidación
### Configuración y Entornos
- Mantener secretos en `.env` y configuración en `config/*.php`
- Usar sobreescrituras de configuración por entorno y `config:cache` en producción
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!