# Laravel Patterns

> When to activate: Laravel, Eloquent ORM, service container, middleware, form requests, resource controllers, artisan, queues, events and listeners, service providers, policies

- Skill: `mattakushi432/laravel-patterns` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mattakushi432/laravel-patterns`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mattakushi432/laravel-patterns/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Mattakushi432 (https://skillmd.com/u/mattakushi432)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mattakushi432/laravel-patterns

---


# 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

```php
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.

```php
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

```php
// 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()

```php
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

```php
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

```php
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 `FormRequest` classes, 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

```php
// 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();
```

```php
// 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.md`
- `skills/php-ecosystem/php-database.md`
- `skills/php-ecosystem/php-testing.md`

