| name | laravel-patterns |
| description | Use when building Laravel applications. Covers application architecture, Eloquent ORM, service layer patterns, form requests, API resources, events/listeners, jobs/queues, middleware, and testing in Laravel 11+. |
| user-invocable | false |
| allowed-tools | ["Read","Write","Bash","Grep","Glob"] |
Laravel Patterns -- Laravel 11+ Best Practices
1. Application Architecture
app/
├── Models/ # Eloquent models with scopes, casts, relationships
├── Services/ # Business logic (one per domain concept)
├── Http/
│ ├── Controllers/ # Thin -- delegate to services
│ ├── Requests/ # Form request validation
│ ├── Resources/ # API response transformers
│ └── Middleware/
├── Policies/ # Authorization rules
├── Events/ # Domain events
├── Listeners/ # Event handlers (often queued)
├── Jobs/ # Background processing
└── Providers/ # AppServiceProvider is primary in L11
public function boot(): void {
Model::shouldBeStrict(! app()->isProduction());
}
2. Service Layer
All business logic in services, never in controllers or models.
class InvoiceService
{
public function __construct(private readonly InventoryService $inventory, private readonly TaxCalculator $tax) {}
public function create(array $data): Invoice
{
return DB::transaction(function () use ($data) {
$invoice = Invoice::create([
'branch_id' => auth()->user()->branch_id,
'customer_id' => $data['customer_id'],
'date' => $data['date'], 'due_date' => $data['due_date'],
'status' => InvoiceStatus::Draft,
]);
foreach ($data['items'] as $item) {
$invoice->items()->create([
=> [],
=> [],
=> [],
=> ->tax->([]),
]);
->inventory->([], [], ::, ->id);
}
->();
( ());
->(, );
});
}
}
3. Eloquent Model (Multi-Tenant)
class Invoice extends Model
{
use SoftDeletes, HasBranch, Auditable;
protected $fillable = ['branch_id', 'customer_id', 'invoice_number', 'date', 'due_date',
'status', 'subtotal', 'tax_total', 'total', 'notes', 'paid_at'];
protected function casts(): array {
return ['date' => 'date', 'due_date' => 'date', 'paid_at' => 'datetime',
'subtotal' => 'decimal:2', 'total' => 'decimal:2', 'status' => InvoiceStatus::class];
}
protected static function booted(): void {
static::addGlobalScope('branch', fn (Builder $q) =>
auth()->check() ? ->(, ()->()->branch_id) : );
::(fn (Invoice ) =>
->invoice_number ??= ::(->branch_id ??= ()->()?->branch_id));
}
{ ->(::); }
{ ->(::); }
{ ->(::); }
{ ->(, [::, ::]); }
{ ->(, ::)->(, , ()); }
{
->subtotal = ->items->(fn () => ->quantity * ->unit_price);
->tax_total = ->items->(fn () => ->quantity * ->unit_price * ->tax_rate);
->total = ->subtotal + ->tax_total;
->();
}
}
HasBranch Trait
trait HasBranch {
public function branch(): BelongsTo { return $this->belongsTo(Branch::class); }
public function scopeForBranch(Builder $q, int $branchId): Builder {
return $q->where($this->getTable() . '.branch_id', $branchId);
}
}
4. Form Request Validation
class CreateInvoiceRequest extends FormRequest
{
public function authorize(): bool { return $this->user()->can('create', Invoice::class); }
public function rules(): array {
$branchId = $this->user()->branch_id;
return [
'customer_id' => ['required', 'integer', Rule::exists('customers', 'id')->where('branch_id', $branchId)],
'date' => ['required', 'date', 'before_or_equal:today'],
'due_date' => ['required', 'date', 'after:date'],
'items' => ['required', 'array', 'min:1', 'max:100'],
'items.*.product_id' => ['required', , ::(, )->(, )],
=> [, , ],
=> [, , ],
];
}
}
Always scope exists rules with branch_id to prevent cross-tenant references.
5. API Resources & Controllers
class InvoiceResource extends JsonResource {
public function toArray(Request $request): array {
return [
'id' => $this->id, 'invoice_number' => $this->invoice_number,
'date' => $this->date->toDateString(), 'status' => $this->status->value,
'total' => (float) $this->total,
'customer' => new CustomerResource($this->whenLoaded('customer')),
'items' => InvoiceItemResource::collection($this->whenLoaded('items')),
'created_at' => $this->created_at->toIso8601String(),
];
}
}
class InvoiceController extends Controller {
public function __construct(private InvoiceService ) {}
{
::(
::()
->(->status, fn (, ) => ->(, ))
->()->(->(, ))
);
}
{
(->service->(->()));
}
}
6. Events, Listeners & Jobs
class InvoiceCreated implements ShouldBroadcast {
use Dispatchable, SerializesModels;
public function __construct(public readonly Invoice $invoice) {}
public function broadcastOn(): Channel { return new PrivateChannel("branch.{$this->invoice->branch_id}"); }
}
class GenerateInvoicePdf implements ShouldQueue {
public function handle(InvoiceCreated $event): void {
$pdf = Pdf::loadView('invoices.pdf', ['invoice' => $event->invoice->load('items.product', 'customer')]);
Storage::disk('s3')->put("invoices//.pdf", ->());
}
}
{
;
= ; = ; = ;
{}
{
::()->(, ->branchId)
->(, ->invoiceIds)->(, fn () => ->(fn () => ->()));
}
}
7. Middleware & Exception Handling
class EnsureBranchScope {
public function handle(Request $request, Closure $next): Response {
abort_unless($request->user()?->branch_id, 403, 'No branch assigned.');
app()->instance('current_branch_id', $request->user()->branch_id);
return $next($request);
}
}
class InsufficientStockException extends DomainException {
public function __construct(public readonly Product $product, public readonly float $requested, public readonly float $available) {
parent::__construct("Insufficient stock for {$product->name}: requested {$requested}, available .");
}
{ ()->([ => ->()], ); }
}
8. Caching
$products = Cache::tags(["branch:{$branchId}", 'products'])
->remember("products:branch:{$branchId}:page:{$page}", now()->addMinutes(15), fn () =>
Product::where('branch_id', $branchId)->paginate(25));
class ProductObserver {
public function saved(Product $p): void { Cache::tags(["branch:{$p->branch_id}", 'products'])->flush(); }
}
9. Testing
public function test_create_invoice_with_items(): void {
$branch = Branch::factory()->create();
$user = User::factory()->for($branch)->create();
$customer = Customer::factory()->for($branch)->create();
$product = Product::factory()->for($branch)->create(['price' => 100.00]);
$this->actingAs($user)->postJson('/api/v1/invoices', [
'customer_id' => $customer->id, 'date' => '2026-03-01', 'due_date' => '2026-03-31',
'items' => [['product_id' => $product->id, 'quantity' => 2, 'unit_price' => 100.00]],
])->assertCreated()->(, );
}
{
= ::()->();
= ::()->()->();
= ::()->();
->()->()->();
}
{
{
[ => ::(), => ::(),
=> . ->faker->()->(),
=> (), => ()->(), => ::,
=> , => ];
}
{ ->([ => ::]); }
{ ->([ => ::, => ()->()]); }
}