
Laravel Patterns
- 1.4k installs
- 238k repo stars
- Updated August 5, 2026
- affaan-m/ecc
This is a copy of laravel-patterns by affaan-m - installs and ranking accrue to the original listing.
laravel-patterns is an ECC skill that enforces production-grade Laravel architecture with clean boundaries between controllers, services, Eloquent models, queues, events, and API resources.
About
laravel-patterns is an ECC-origin agent skill for production Laravel architecture covering routing, controllers, Eloquent ORM, service layers, background queues, events, caching, and API resources with pagination. Developers reach for it when building Laravel web apps or APIs, structuring domain logic away from fat controllers, designing Eloquent relationships, or adding jobs and event-driven workflows. The skill enforces clear boundaries—controllers delegate to services, models stay thin, and API responses use resources with consistent pagination. It activates during greenfield Laravel builds and refactors where maintainable layer separation prevents common Laravel anti-patterns.
- Enforces layered architecture (controllers → services/actions → models)
- Covers Eloquent ORM, typed models, casts, scopes and relationships
- Includes queues, events, caching strategies and background jobs
- Standardizes API resources, pagination, form requests and policies
- Provides explicit routing bindings while maintaining authorization
Laravel Patterns by the numbers
- 1,407 all-time installs (skills.sh)
- +87 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/affaan-m/ecc --skill laravel-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.4k |
|---|---|
| repo stars | ★ 238k |
| Last updated | August 5, 2026 |
| Repository | affaan-m/ecc ↗ |
How do you structure production Laravel applications?
Get production-grade Laravel architecture patterns that enforce clean boundaries between controllers, services, models, and jobs.
Who is it for?
PHP developers building Laravel APIs or web apps who need enforced controller-service-model boundaries and queue/event patterns.
Skip if: Non-PHP stacks or minimal Laravel prototypes that intentionally skip service layers and background job infrastructure.
When should I use this skill?
A Laravel app needs controller-service boundaries, Eloquent relationship design, API resources, or queue and event infrastructure.
What you get
Layered Laravel modules with service classes, Eloquent models, queue jobs, event handlers, and paginated API resources.
- layered Laravel modules
- API resource definitions
Files
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.phpControladores -> Servicios -> Actions
Mantener los controladores delgados. Poner la orquestación en servicios y la lógica de un solo propósito en actions.
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.
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.
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.
conversationvsconversations). - Usar un único nombre de parámetro que coincida con el modelo vinculado (ej.
{conversation}paraConversation). - Preferir bindings con scope al anidar para aplicar relaciones padre-hijo.
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.
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.
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
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.
use Illuminate\Database\Eloquent\Casts\Attribute;
protected $casts = [
'status' => ProjectStatus::class,
];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
$orders = Order::query()
->with(['customer', 'items.product'])
->latest()
->paginate(25);Query Objects para Filtros Complejos
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.
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
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
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_casey plurales por defecto
Ejemplo de Migración
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.
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.
$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
.envy configuración enconfig/*.php - Usar sobreescrituras de configuración por entorno y
config:cacheen producción
Related skills
How it compares
Choose this over generic PHP skills when agents need Laravel-specific service boundaries, Eloquent patterns, and API resource conventions.
FAQ
What Laravel layers does laravel-patterns cover?
laravel-patterns covers routing, controllers, Eloquent ORM, service layers, background queues, events, caching, and API resources with pagination for production Laravel applications.
When should laravel-patterns be used?
laravel-patterns should be used when building Laravel web or API applications, structuring domain logic, designing Eloquent relationships, or adding queues and event-driven background jobs.