Laravel Patterns
When to Use
Building or reviewing a Laravel application: controllers, Eloquent models, form requests, service providers, jobs, or authorization policies.
Core Patterns
Thin Controllers, Form Requests for Validation
final class StoreOrderRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('create', Order::class);
}
public function rules(): array
{
return [
'customer_id' => ['required', 'exists:customers,id'],
'items' => ['required', 'array', 'min:1'],
'items.*.sku' => ['required', 'string', 'exists:products,sku'],
];
}
}
final class OrderController extends Controller
{
public function __construct(private readonly CreateOrder $createOrder)
{
}
public function store(StoreOrderRequest $request): JsonResponse
{
$order = $this->createOrder->handle($request->validated());
return OrderResource::make($order)->response()->setStatusCode(201);
}
}
Service/Action Classes Over Fat Models
Push business logic out of controllers and models into single-purpose action classes.
final class CreateOrder
{
public function __construct(
private readonly OrderRepository $orders,
private readonly InventoryService $inventory,
) {
}
public function handle(array $data): Order
{
return DB::transaction(function () use ($data) {
$this->inventory->reserve($data['items']);
return $this->orders->create($data);
});
}
}
Eloquent: Avoid N+1 with Eager Loading
// BAD: N+1 query — one query per order for its customer
$orders = Order::all();
foreach ($orders as $order) {
echo $order->customer->name;
}
// GOOD: eager load the relation
$orders = Order::with('customer', 'items.product')->get();
// Enforce it in local development
Model::preventLazyLoading(! app()->isProduction());
Query Scopes Instead of Repeated where()
final class Order extends Model
{
public function scopePending(Builder $query): Builder
{
return $query->where('status', OrderStatus::Pending);
}
public function scopeForCustomer(Builder $query, Customer $customer): Builder
{
return $query->where('customer_id', $customer->id);
}
}
Order::pending()->forCustomer($customer)->get();
Policies for Authorization
final class OrderPolicy
{
public function view(User $user, Order $order): bool
{
return $user->id === $order->customer_id || $user->isAdmin();
}
}
// In a controller or Blade template
$this->authorize('view', $order);
@can('view', $order) ... @endcan
Events and Listeners for Side Effects
final class OrderShipped
{
public function __construct(public readonly Order $order)
{
}
}
final class SendShippingNotification
{
public function handle(OrderShipped $event): void
{
Mail::to($event->order->customer)->send(new OrderShippedMail($event->order));
}
}
event(new OrderShipped($order));
Checklist
- Validation lives in
FormRequestclasses, not inline in controllers - Business logic in action/service classes, not in controllers or models
-
Model::preventLazyLoading()enabled outside production - Authorization goes through Policies/Gates, not manual
if ($user->id === ...)checks scattered in controllers - Queries use scopes for reused filters
- Mass-assignment protected via
$fillable(never$guarded = []) - Migrations are reversible (
down()implemented)
Anti-Patterns
// BAD: raw SQL fragments with interpolated variables
DB::select("SELECT * FROM orders WHERE status = '$status'");
// GOOD: query builder / Eloquent, parameter binding
Order::where('status', $status)->get();
// BAD: business logic and cross-cutting concerns in the model
class Order extends Model {
public function ship() {
$this->update(['status' => 'shipped']);
Mail::to($this->customer)->send(new OrderShippedMail($this));
Log::info('order shipped');
}
}
// GOOD: model stays a thin persistence layer; orchestration in an action class
Quick Reference
| Concern | Laravel Tool |
|---|---|
| Input validation | FormRequest |
| Authorization | Policy / Gate |
| Cross-cutting side effects | Events + Listeners |
| Background work | Queued Jobs |
| Reused query filters | Local Scopes |
| API shape | JsonResource |
| Config-driven wiring | ServiceProvider |
See Also
skills/php-ecosystem/php-patterns.mdskills/php-ecosystem/php-database.mdskills/php-ecosystem/php-testing.md