Claude Skill

laravel-ops

Laravel framework patterns, Eloquent ORM, authentication, queues, and testing. Use for: laravel, eloquent, artisan, blade, php, sanctum, livewire, inertia, pest, phpunit, forge, vapor, queue, middleware, migration, factory, seeder.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download 0xdarkmatter-claude-mods-skills_laravel-ops-3dfaf0b.zip · 30 KB
Part of 0xdarkmatter/claude-mods — 94 skills

Install

skills CLI npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/laravel-ops
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart
Git git clone https://github.com/0xDarkMatter/claude-mods.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole 0xdarkmatter/claude-mods collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Laravel Operations

Facts verified as of 2026-07.

Authoritative reference for Laravel 11+ development: architecture decisions, Eloquent patterns, authentication strategies, queue configuration, and testing approaches.


Architecture Decision Tree

What type of application?
│
├─ Full-stack web (HTML responses)
│  ├─ Simple CRUD, small team → Monolith (Blade + Eloquent directly)
│  │   └─ Use action classes for business logic over 20 lines
│  ├─ Rich interactivity needed → Livewire (server-driven reactivity)
│  │   └─ Add Alpine.js for client-side micro-interactions
│  └─ SPA-like feel, React/Vue team → Inertia.js
│      └─ Keep server-side routing, dump client-side routing overhead
│
├─ API backend (JSON responses)
│  ├─ Single consumer (mobile/SPA) → API-only with Sanctum SPA auth
│  ├─ Multiple consumers / public → RESTful API with token auth
│  └─ Complex graph queries → Consider GraphQL (lighthouse-php/lighthouse)
│
├─ Large team / complex domain
│  ├─ Domain-driven → Modular monolith (app/Modules/{Domain}/)
│  │   ├─ Each module: Models, Actions, Events, Jobs, Http/
│  │   └─ Shared: app/Shared/ for cross-cutting concerns
│  └─ Independent deployability needed → Microservices
│      └─ Use Laravel Octane for high-throughput services
│
└─ What business logic pattern?
   ├─ Simple CRUD, < 20 lines → Direct Eloquent in controller
   ├─ Reusable operation (create order, send invoice) → Action class
   │   └─ Single public handle() or execute() method
   ├─ Complex queries, multiple data sources → Repository pattern
   │   └─ Interface + Eloquent implementation (enables swapping)
   └─ Cross-cutting operations (audit, caching) → Service class
       └─ Inject via constructor, bind in ServiceProvider

Action Class vs Repository vs Service

Pattern Use When Example
Action class Single, reusable business operation CreateOrderAction, SendInvoiceAction
Repository Abstract data access, multiple sources OrderRepository with EloquentOrderRepository
Service Orchestrate multiple actions/repos OrderService combining payment + inventory
Direct Eloquent Simple CRUD, < 5 lines in controller User::create($data)

Eloquent Quick Reference

Relationships

Relationship Method Foreign Key Convention
hasOne return $this->hasOne(Profile::class) profiles.user_id
hasMany return $this->hasMany(Post::class) posts.user_id
belongsTo return $this->belongsTo(User::class) posts.user_id
belongsToMany return $this->belongsToMany(Role::class) role_user pivot
hasManyThrough return $this->hasManyThrough(Post::class, User::class) Country → User → Post
morphTo return $this->morphTo() {col}_type, {col}_id
morphMany return $this->morphMany(Comment::class, 'commentable') Polymorphic
morphToMany return $this->morphToMany(Tag::class, 'taggable') Polymorphic pivot

Eager Loading

// Prevent N+1: always eager load in controllers
$posts = Post::with(['author', 'comments.author', 'tags'])->paginate(15);

// Conditional eager loading (load after retrieval)
$user->load('posts.comments');
$user->loadMissing('posts'); // only if not already loaded

// Eager load counts (no SELECT *)
$posts = Post::withCount('comments')->get();

// Constrained eager loading
$posts = Post::with(['comments' => fn($q) => $q->approved()->latest()])->get();

Query Scopes

// Local scope (reusable query constraint)
public function scopeActive(Builder $query): void
{
    $query->where('status', 'active');
}

// Usage: User::active()->get()

// Dynamic scope
public function scopeOfType(Builder $query, string $type): void
{
    $query->where('type', $type);
}
// Usage: User::ofType('admin')->get()

Mass Assignment

// Fillable (allowlist - preferred)
protected $fillable = ['name', 'email', 'password'];

// Guarded (denylist - use [] only if you trust all input)
protected $guarded = ['id', 'is_admin'];

// Never set guarded = [] in production code

Artisan Command Cheat Sheet

Command Purpose Common Options
make:model Post -mfs Model + migration + factory + seeder -c controller, -r resource
make:controller PostController -r Resource controller (7 methods) --api skips create/edit
make:request StorePostRequest Form request for validation
make:job ProcessPayment Queueable job class --sync for sync job
make:event OrderPlaced Event class
make:listener SendOrderConfirmation -e OrderPlaced Listener for event --queued
make:notification InvoicePaid Notification class
make:policy PostPolicy -m Post Policy with model
make:middleware EnsureUserIsAdmin HTTP middleware
make:command SendDailyReport Custom Artisan command
migrate Run pending migrations --step for individual
migrate:rollback Roll back last batch --step=5
migrate:fresh --seed Drop all + re-migrate + seed
db:seed Run all seeders --class=UserSeeder
tinker REPL with app context
route:list Show all routes --name=api filter
route:cache Cache routes for production
config:cache Cache config for production
view:cache Pre-compile Blade templates
optimize Run all cache commands optimize:clear to reset
queue:work Process queue jobs --queue=high,default
queue:listen Work + auto-reload on code change
queue:failed List failed jobs
queue:retry all Retry all failed jobs
schedule:run Run due scheduled tasks
schedule:work Run scheduler every minute (dev)
key:generate Generate APP_KEY
test Run PHPUnit/Pest tests --filter=UserTest
test --parallel Run tests in parallel --processes=4
vendor:publish Publish package assets/config --tag=config

Authentication Decision Tree

What do you need?
│
├─ SPA (Vue/React) + Laravel API backend
│  └─ Sanctum SPA authentication
│     ├─ Cookie-based (same domain or subdomain)
│     ├─ Csrf-cookie endpoint: GET /sanctum/csrf-cookie
│     └─ No tokens in localStorage (XSS safe)
│
├─ Mobile app or third-party API consumers
│  └─ Sanctum API tokens (Bearer tokens)
│     ├─ createToken($name, $abilities)
│     ├─ Token abilities for fine-grained control
│     └─ Token expiration with token:prune schedule
│
├─ Traditional web app (server-rendered)
│  ├─ Just need auth pages quickly → Breeze
│  │   ├─ Minimal, educational, Blade or Inertia stack
│  │   └─ Install: composer require laravel/breeze --dev
│  ├─ Need teams, 2FA, profile management → Jetstream
│  │   ├─ Livewire or Inertia stack
│  │   └─ Install: composer require laravel/jetstream
│  └─ Need headless auth (API + custom UI) → Fortify
│      ├─ Actions in app/Actions/Fortify/
│      └─ Customize: CreateNewUser, UpdateUserPassword
│
└─ Custom / enterprise
   ├─ LDAP/SAML → socialiteproviders/saml2
   ├─ OAuth social login → laravel/socialite
   └─ Custom guard → Implement Guard + UserProvider contracts

Sanctum Quick Setup

// config/sanctum.php - stateful domains for SPA
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost')),

// API token creation
$token = $user->createToken('mobile-app', ['orders:read', 'orders:write']);
return ['token' => $token->plainTextToken];

// Check token ability
Route::get('/orders', function (Request $request) {
    $request->user()->tokenCan('orders:read'); // bool
});

// Protect routes
Route::middleware('auth:sanctum')->group(function () {
    // authenticated routes
});

Queue Decision Tree

Queue driver selection:
│
├─ Development / testing
│  └─ sync driver (executes immediately, no worker needed)
│     QUEUE_CONNECTION=sync
│
├─ Small app, no Redis available
│  └─ database driver
│     ├─ php artisan queue:table && migrate
│     ├─ Works fine for < 100 jobs/min
│     └─ QUEUE_CONNECTION=database
│
├─ Medium-high throughput, self-hosted
│  └─ Redis driver (via predis or phpredis)
│     ├─ QUEUE_CONNECTION=redis
│     ├─ Laravel Horizon for monitoring
│     └─ Supports priorities, pausing, metrics
│
└─ AWS infrastructure / massive scale
   └─ SQS driver
      ├─ QUEUE_CONNECTION=sqs
      ├─ Managed, auto-scaling
      └─ Use with Laravel Vapor for serverless

Job Patterns

// Basic job dispatch
ProcessPayment::dispatch($order);
ProcessPayment::dispatch($order)->onQueue('payments')->delay(now()->addMinutes(5));

// Chaining (sequential)
Bus::chain([
    new ProcessPayment($order),
    new SendInvoice($order),
    new UpdateInventory($order),
])->dispatch();

// Batching (parallel + callback)
$batch = Bus::batch([
    new ImportRow($row1),
    new ImportRow($row2),
    new ImportRow($row3),
])->then(fn(Batch $batch) => ImportComplete::dispatch())
  ->catch(fn(Batch $batch, Throwable $e) => Log::error($e))
  ->dispatch();

// Rate limiting (throttle to 5 per minute)
public function middleware(): array
{
    return [new RateLimited('payments')];
}

// Unique jobs (prevent duplicate processing)
use Illuminate\Contracts\Queue\ShouldBeUnique;

class ProcessPayment implements ShouldQueue, ShouldBeUnique
{
    public string $uniqueId => $this->order->id;
    public int $uniqueFor = 3600; // seconds
}

// Retry configuration
public int $tries = 3;
public int $backoff = 60; // seconds between retries

public function retryUntil(): DateTime
{
    return now()->addHours(24);
}

Task Scheduling

// routes/console.php (Laravel 11+)
Schedule::job(SendDailyReport::class)->dailyAt('08:00')->timezone('America/New_York');
Schedule::command('backup:run')->daily()->runInBackground()->emailOutputOnFailure('ops@app.com');
Schedule::call(fn() => Cache::flush())->weekly()->sundays()->at('00:00');

// Prevent overlap (long-running tasks)
Schedule::job(ProcessImport::class)->everyFiveMinutes()->withoutOverlapping();

// Run on one server only (requires Redis/database cache driver)
Schedule::job(SendNewsletters::class)->daily()->onOneServer();

Testing Quick Reference

Test Types

Type Class extends Database Purpose
Feature test Tests\TestCase Yes (with trait) HTTP endpoints, full stack
Unit test PHPUnit\Framework\TestCase No Pure logic, no app boot
Browser test Laravel\Dusk\TestCase Yes Real browser via ChromeDriver

Database Traits

use Illuminate\Foundation\Testing\RefreshDatabase;   // migrate fresh each test (slower)
use Illuminate\Foundation\Testing\DatabaseTransactions; // rollback each test (faster)

Pest Syntax (preferred in Laravel 11+)

describe('User authentication', function () {
    beforeEach(function () {
        $this->user = User::factory()->create();
    });

    it('allows login with valid credentials', function () {
        $response = $this->post('/login', [
            'email' => $this->user->email,
            'password' => 'password',
        ]);

        $response->assertRedirect('/dashboard');
        $this->assertAuthenticatedAs($this->user);
    });

    it('rejects invalid credentials')->todo();
});

Common Assertions

// HTTP response
$response->assertStatus(200);
$response->assertOk();             // 200
$response->assertCreated();        // 201
$response->assertNoContent();      // 204
$response->assertUnauthorized();   // 401
$response->assertForbidden();      // 403
$response->assertNotFound();       // 404
$response->assertRedirect('/home');

// JSON responses
$response->assertJson(['status' => 'ok']);
$response->assertJsonPath('data.email', 'user@example.com');
$response->assertJsonCount(3, 'data');
$response->assertJsonStructure(['data' => ['id', 'name', 'email']]);
$response->assertJsonMissing(['password']);

// Database
$this->assertDatabaseHas('users', ['email' => 'user@example.com']);
$this->assertDatabaseMissing('users', ['email' => 'deleted@example.com']);
$this->assertDatabaseCount('posts', 5);
$this->assertSoftDeleted('posts', ['id' => $post->id]);

Common Gotchas

Gotcha Why Fix
N+1 queries on relationships Eloquent lazy-loads by default Use with() eager loading; enable Model::preventLazyLoading() in AppServiceProvider during development
Mass assignment vulnerability $fillable = [] accepts all Always define $fillable; never use $guarded = [] in production
created_at not updating on update() Only updated_at auto-sets Use $model->touch() or timestamps = true (default)
Queue job fails on model serialization Model state may change between dispatch and processing Use SerializesModels trait; re-fetch from DB in handle() if needed
Timezone mismatch in scheduled tasks Server tz != app tz Set APP_TIMEZONE in .env; use ->timezone() on schedule entries
Middleware order matters Auth middleware must run before policies Global → route group → route. Auth before throttle check or vice versa changes 401 vs 429
Route model binding skips soft-deleted records RouteServiceProvider ignores trashed() Extend binding: Route::bind('post', fn($id) => Post::withTrashed()->findOrFail($id))
Service container binding not auto-resolved Interface not bound to implementation Register in AppServiceProvider::register(): $this->app->bind(Interface::class, Implementation::class)
Migration foreign key order Must create referenced table first Run migrate:fresh to verify; use Schema::disableForeignKeyConstraints() in tests
CSRF protection blocks API routes VerifyCsrfToken runs on all web routes Register API routes in routes/api.php (uses api middleware group without CSRF)
env() returns null after caching config:cache bakes env values Always access env via config() helper in app code; only use env() in config/ files
Blade @stack renders in wrong order @push must appear after @stack in execution Use @prepend for scripts that must appear first
Event listener not firing Listener not registered or discovered Check EventServiceProvider::$listen; or enable Event::discover() in Laravel 11

Reference Files

File Contents
references/eloquent-queries.md Deep-dive: relationships, query builder, scopes, accessors, mutators, events, soft deletes, pagination, performance, collections, factories
references/architecture.md Service container, providers, facades, middleware, events, notifications, jobs, scheduling, Blade components, Livewire, Inertia
references/testing-auth.md PHPUnit/Pest setup, HTTP tests, database testing, fakes, Sanctum, Fortify, policies, form requests, Dusk

See Also

  • sql-ops - Query optimization, indexing strategy, raw SQL patterns
  • postgres-ops - PostgreSQL-specific features, JSON columns, full-text search
  • testing-ops - General testing philosophy, TDD, CI integration
  • docker-ops - Containerizing Laravel apps, Docker Compose, production setup

Key External Resources

Files (claude-mods)
  • assets
    • .gitkeep 0 B · in bundle
  • references
    • architecture.md 24.2 KB
      # Laravel Architecture Reference
      
      Deep-dive reference for Laravel 11+ architecture: service container, providers, facades, middleware, events, notifications, jobs, scheduling, Blade, Livewire, and Inertia.
      
      ---
      
      ## Service Container
      
      The container resolves class dependencies automatically via reflection.
      
      ### Binding Types
      
      ```php
      // AppServiceProvider::register()
      
      // Bind (new instance each resolution)
      $this->app->bind(PaymentGateway::class, StripeGateway::class);
      $this->app->bind(PaymentGateway::class, function ($app) {
          return new StripeGateway($app->make(HttpClient::class), config('stripe.key'));
      });
      
      // Singleton (same instance every resolution)
      $this->app->singleton(AnalyticsService::class, function ($app) {
          return new AnalyticsService($app->make(Logger::class));
      });
      
      // Instance (bind a pre-existing object)
      $this->app->instance(Config::class, new Config(['debug' => true]));
      
      // Scoped (singleton per request lifecycle - useful with Octane)
      $this->app->scoped(RequestContext::class, function ($app) {
          return new RequestContext($app->make(Request::class));
      });
      ```
      
      ### Contextual Binding
      
      ```php
      // Give different implementations to different classes
      $this->app->when(PhotoController::class)
                ->needs(Filesystem::class)
                ->give(fn() => Storage::disk('photos'));
      
      $this->app->when(VideoController::class)
                ->needs(Filesystem::class)
                ->give(fn() => Storage::disk('videos'));
      
      // Bind tagged implementations
      $this->app->bind(CsvReport::class, fn() => new CsvReport());
      $this->app->bind(PdfReport::class, fn() => new PdfReport());
      $this->app->tag([CsvReport::class, PdfReport::class], 'reports');
      
      $reports = $this->app->tagged('reports'); // array of resolved instances
      ```
      
      ### Auto-Resolution and Method Injection
      
      ```php
      // Constructor injection (auto-resolved)
      class OrderService
      {
          public function __construct(
              private readonly PaymentGateway $payment,
              private readonly InventoryRepository $inventory,
              private readonly EventDispatcher $events,
          ) {}
      }
      
      // Call with method injection
      $result = app()->call([OrderService::class, 'process'], ['orderId' => 123]);
      
      // Resolve with makeWith (pass primitives)
      $service = app()->makeWith(ReportService::class, ['format' => 'pdf']);
      ```
      
      ---
      
      ## Service Providers
      
      ### Structure
      
      ```php
      class AppServiceProvider extends ServiceProvider
      {
          // Bindings array - simple alias
          public array $bindings = [
              OrderRepositoryInterface::class => EloquentOrderRepository::class,
          ];
      
          // Singletons array
          public array $singletons = [
              CurrencyConverter::class => CurrencyConverter::class,
          ];
      
          // register(): bind into container (no other services available yet)
          public function register(): void
          {
              $this->app->bind(PaymentGateway::class, fn($app) => new StripeGateway(
                  config('services.stripe.key')
              ));
          }
      
          // boot(): everything is registered, safe to use facades and other services
          public function boot(): void
          {
              Model::preventLazyLoading(! $this->app->isProduction());
              Blade::directive('money', fn($amount) => "<?php echo money_format({$amount}); ?>");
              Post::observe(PostObserver::class);
              Validator::extend('phone', [PhoneValidator::class, 'validate']);
          }
      }
      ```
      
      ### Deferred Providers
      
      ```php
      // Only loaded when the binding is actually requested
      class ReportServiceProvider extends ServiceProvider implements DeferrableProvider
      {
          public function register(): void
          {
              $this->app->singleton(ReportGenerator::class, fn() => new ReportGenerator());
          }
      
          public function provides(): array
          {
              return [ReportGenerator::class]; // what this provider resolves
          }
      }
      ```
      
      ### Package Service Providers
      
      ```php
      class PackageServiceProvider extends ServiceProvider
      {
          public function register(): void
          {
              $this->mergeConfigFrom(__DIR__.'/../config/package.php', 'package');
          }
      
          public function boot(): void
          {
              // Publish config
              $this->publishes([
                  __DIR__.'/../config/package.php' => config_path('package.php'),
              ], 'config');
      
              // Publish migrations
              $this->publishes([
                  __DIR__.'/../database/migrations' => database_path('migrations'),
              ], 'migrations');
      
              // Load migrations without publishing
              $this->loadMigrationsFrom(__DIR__.'/../database/migrations');
      
              // Load routes
              $this->loadRoutesFrom(__DIR__.'/../routes/web.php');
      
              // Load views (with namespace prefix)
              $this->loadViewsFrom(__DIR__.'/../resources/views', 'package');
          }
      }
      ```
      
      ---
      
      ## Facades
      
      Facades provide a static interface to services in the container.
      
      ```php
      // How facades work internally
      Cache::get('key');
      // resolves to: app('cache')->get('key')
      
      // Real-time facades (prefix with Facades\)
      use Facades\App\Services\PaymentGateway;
      PaymentGateway::charge($amount); // automatically resolved from container
      
      // All standard facades
      use Illuminate\Support\Facades\{
          App, Artisan, Auth, Blade, Bus, Cache, Config, Cookie, Crypt,
          DB, Event, File, Gate, Hash, Http, Log, Mail, Notification,
          Queue, Redirect, Request, Response, Route, Schema, Session,
          Storage, URL, Validator, View
      };
      ```
      
      ### Testing with Facade Fakes
      
      ```php
      // In test setup - swap real implementation with fake
      Event::fake();
      Mail::fake();
      Notification::fake();
      Queue::fake();
      Bus::fake();
      Storage::fake('s3');
      Http::fake(['api.stripe.com/*' => Http::response(['id' => 'ch_123'], 200)]);
      
      // Then assert interactions
      Event::assertDispatched(OrderPlaced::class, fn($e) => $e->order->id === $orderId);
      Event::assertNotDispatched(OrderCancelled::class);
      Mail::assertSent(InvoiceMail::class, fn($mail) => $mail->hasTo('user@example.com'));
      Notification::assertSentTo($user, InvoicePaidNotification::class);
      Queue::assertPushed(ProcessPayment::class, fn($job) => $job->order->id === $orderId);
      Queue::assertPushedOn('high-priority', ProcessPayment::class);
      ```
      
      ---
      
      ## Middleware
      
      ### Defining Middleware
      
      ```php
      // php artisan make:middleware EnsureUserIsSubscribed
      
      class EnsureUserIsSubscribed
      {
          public function handle(Request $request, Closure $next): Response
          {
              if (! $request->user()?->subscribed()) {
                  return redirect('/billing')->with('error', 'Subscription required.');
              }
      
              return $next($request);
          }
      }
      
      // Middleware with parameters
      class EnsureRole
      {
          public function handle(Request $request, Closure $next, string ...$roles): Response
          {
              if (! $request->user()->hasAnyRole($roles)) {
                  abort(403);
              }
              return $next($request);
          }
      }
      // Route: Route::middleware('role:admin,editor')->group(...)
      ```
      
      ### Registering Middleware (Laravel 11+)
      
      ```php
      // bootstrap/app.php
      ->withMiddleware(function (Middleware $middleware) {
          // Global middleware
          $middleware->append(LogHttpRequests::class);
          $middleware->prepend(TrustProxies::class);
      
          // Named middleware aliases
          $middleware->alias([
              'subscribed' => EnsureUserIsSubscribed::class,
              'role'       => EnsureRole::class,
          ]);
      
          // Middleware groups
          $middleware->group('api', [
              ThrottleRequests::class.':api',
              SubstituteBindings::class,
          ]);
      
          // Exclude from global middleware
          $middleware->except([VerifyCsrfToken::class], ['/webhooks/*']);
      })
      ```
      
      ### Terminable Middleware
      
      ```php
      // Runs AFTER response is sent (for cleanup, logging)
      class LogResponseTime implements TerminableMiddleware
      {
          private float $startTime;
      
          public function handle(Request $request, Closure $next): Response
          {
              $this->startTime = microtime(true);
              return $next($request);
          }
      
          public function terminate(Request $request, Response $response): void
          {
              $duration = microtime(true) - $this->startTime;
              Log::channel('performance')->info('Request completed', [
                  'url'      => $request->fullUrl(),
                  'duration' => round($duration * 1000, 2) . 'ms',
                  'status'   => $response->getStatusCode(),
              ]);
          }
      }
      ```
      
      ### Rate Limiting
      
      ```php
      // AppServiceProvider::boot() or RouteServiceProvider
      RateLimiter::for('api', function (Request $request) {
          return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
      });
      
      RateLimiter::for('uploads', function (Request $request) {
          return [
              Limit::perMinute(10)->by($request->user()->id),     // per user
              Limit::perDay(100)->by($request->user()->id),       // daily cap
          ];
      });
      
      // Route-level: Route::middleware('throttle:api')->group(...)
      ```
      
      ---
      
      ## Events and Listeners
      
      ### Event Classes
      
      ```php
      // php artisan make:event OrderPlaced
      class OrderPlaced
      {
          use Dispatchable, InteractsWithSockets, SerializesModels;
      
          public function __construct(
              public readonly Order $order,
              public readonly User $customer,
          ) {}
      
          // Broadcast over WebSockets (optional)
          public function broadcastOn(): array
          {
              return [new PrivateChannel("orders.{$this->order->id}")];
          }
      }
      ```
      
      ### Listener Classes
      
      ```php
      // php artisan make:listener SendOrderConfirmation --event=OrderPlaced
      class SendOrderConfirmation implements ShouldQueue
      {
          use InteractsWithQueue;
      
          public string $queue = 'notifications';
          public int $tries = 3;
      
          public function handle(OrderPlaced $event): void
          {
              Mail::to($event->customer)->send(new OrderConfirmationMail($event->order));
          }
      
          public function failed(OrderPlaced $event, Throwable $exception): void
          {
              Log::error('Failed to send order confirmation', ['order_id' => $event->order->id]);
          }
      }
      ```
      
      ### Event Discovery (Laravel 11+)
      
      ```php
      // bootstrap/app.php - auto-discover listeners in app/Listeners
      ->withEvents(function (Dispatcher $events) {
          $events->listen(OrderPlaced::class, SendOrderConfirmation::class);
          $events->listen(OrderPlaced::class, UpdateInventory::class);
          // Or enable auto-discovery:
          // $events->discover(app_path('Listeners'));
      })
      
      // Dispatch
      OrderPlaced::dispatch($order, $user);
      event(new OrderPlaced($order, $user));  // equivalent
      ```
      
      ---
      
      ## Notifications
      
      ### Notification Class
      
      ```php
      // php artisan make:notification InvoicePaid
      class InvoicePaid extends Notification implements ShouldQueue
      {
          public function __construct(private readonly Invoice $invoice) {}
      
          // Which channels to send on
          public function via(object $notifiable): array
          {
              return $notifiable->prefers_sms
                  ? ['mail', 'vonage']
                  : ['mail', 'database'];
          }
      
          // Email channel
          public function toMail(object $notifiable): MailMessage
          {
              return (new MailMessage)
                  ->subject("Invoice #{$this->invoice->number} paid")
                  ->greeting("Hello {$notifiable->name},")
                  ->line("Your invoice of {$this->invoice->amount_formatted} has been paid.")
                  ->action('View Invoice', route('invoices.show', $this->invoice))
                  ->line('Thank you for your business!');
          }
      
          // Database channel
          public function toDatabase(object $notifiable): array
          {
              return [
                  'invoice_id' => $this->invoice->id,
                  'amount'     => $this->invoice->amount,
                  'paid_at'    => now()->toISOString(),
              ];
          }
      
          // Vonage (SMS) channel
          public function toVonage(object $notifiable): VonageMessage
          {
              return (new VonageMessage)
                  ->content("Invoice #{$this->invoice->number} paid. Amount: {$this->invoice->amount_formatted}");
          }
      
          // Slack channel (via laravel/slack-notification-channel)
          public function toSlack(object $notifiable): SlackMessage
          {
              return (new SlackMessage)
                  ->success()
                  ->content("Invoice paid: #{$this->invoice->number}");
          }
      }
      
      // Sending
      $user->notify(new InvoicePaid($invoice));               // via model
      Notification::send($users, new InvoicePaid($invoice));  // to collection
      Notification::route('mail', 'ops@app.com')              // on-demand
                   ->notify(new InvoicePaid($invoice));
      
      // Database notifications
      $user->unreadNotifications;
      $user->notifications()->markAsRead();
      ```
      
      ---
      
      ## Jobs and Queues
      
      ### Job Class Structure
      
      ```php
      // php artisan make:job ProcessPayment
      class ProcessPayment implements ShouldQueue
      {
          use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
      
          public int $tries = 3;
          public int $timeout = 90;
          public int $backoff = 60;           // seconds between retries
          public bool $deleteWhenMissingModels = true;
      
          public function __construct(
              private readonly Order $order,
              private readonly string $paymentMethodId,
          ) {}
      
          public function handle(PaymentGateway $gateway): void
          {
              // Re-fetch model (may have changed since dispatch)
              $order = Order::find($this->order->id);
      
              $charge = $gateway->charge($order->total, $this->paymentMethodId);
              $order->update(['payment_id' => $charge->id, 'status' => 'paid']);
              OrderPaid::dispatch($order);
          }
      
          // Exponential backoff per attempt
          public function backoff(): array
          {
              return [30, 60, 120]; // wait 30s, 60s, 120s between retries
          }
      
          // Called when all retries exhausted
          public function failed(Throwable $exception): void
          {
              $this->order->update(['status' => 'payment_failed']);
              Log::error('Payment failed', ['order_id' => $this->order->id, 'error' => $exception->getMessage()]);
          }
      
          // Middleware on job
          public function middleware(): array
          {
              return [
                  new RateLimited('payments'),
                  new WithoutOverlapping($this->order->id), // prevent duplicate processing
              ];
          }
      }
      
      // Dispatch patterns
      ProcessPayment::dispatch($order, $paymentMethodId);
      ProcessPayment::dispatch($order, $paymentMethodId)->onQueue('payments');
      ProcessPayment::dispatch($order, $paymentMethodId)->delay(now()->addSeconds(30));
      ProcessPayment::dispatchSync($order, $paymentMethodId); // synchronous (bypasses queue)
      ProcessPayment::dispatchIf($order->requiresPayment(), $order, $paymentMethodId);
      ProcessPayment::dispatchUnless($order->isFree(), $order, $paymentMethodId);
      ```
      
      ### Batches and Chains
      
      ```php
      // Chain (sequential - each waits for previous to complete)
      Bus::chain([
          new ValidateOrder($order),
          new ProcessPayment($order, $method),
          new SendConfirmation($order),
      ])->onQueue('orders')
        ->catch(fn(Throwable $e) => $order->markAsFailed($e->getMessage()))
        ->dispatch();
      
      // Batch (parallel - all run concurrently)
      $batch = Bus::batch(
          $rows->map(fn($row) => new ImportRow($row))->all()
      )->then(function (Batch $batch) {
          ImportComplete::dispatch($batch->id);
      })->catch(function (Batch $batch, Throwable $e) {
          Log::error('Batch failed', ['id' => $batch->id]);
      })->finally(function (Batch $batch) {
          // always runs
      })->name('CSV Import')
        ->allowFailures()          // don't cancel on single failure
        ->onQueue('imports')
        ->dispatch();
      
      // Monitor batch
      $batch = Bus::findBatch($batchId);
      $batch->totalJobs;           // int
      $batch->processedJobs();     // int
      $batch->failedJobs;          // int
      $batch->progress();          // 0-100
      $batch->finished();          // bool
      ```
      
      ---
      
      ## Task Scheduling
      
      ```php
      // routes/console.php (Laravel 11+)
      use Illuminate\Support\Facades\Schedule;
      
      // Frequency methods
      Schedule::job(GenerateSitemap::class)->daily();
      Schedule::job(SendNewsletters::class)->weekdays()->at('08:00');
      Schedule::command('reports:monthly')->monthlyOn(1, '00:30');
      Schedule::command('cache:prune')->everyFiveMinutes()->withoutOverlapping(10); // lock for 10 min max
      Schedule::call(fn() => DB::table('logs')->where('created_at', '<', now()->subDays(90))->delete())
               ->weekly()->sundays();
      
      // Output and notification
      Schedule::command('backup:run')
               ->daily()
               ->runInBackground()
               ->appendOutputTo(storage_path('logs/backup.log'))
               ->emailOutputOnFailure('ops@app.com')
               ->pingOnSuccess(env('HEALTHCHECK_URL'));
      
      // Run on one server (distributed lock via cache)
      Schedule::job(SendDailyDigest::class)->daily()->onOneServer();
      
      // Environment constraints
      Schedule::command('sync:users')->hourly()->environments(['production']);
      
      // Maintenance mode bypass
      Schedule::job(HeartbeatCheck::class)->everyMinute()->evenInMaintenanceMode();
      
      // Chained callbacks
      Schedule::call(function () {
          // ...
      })->before(fn() => Log::info('Starting'))
        ->after(fn() => Log::info('Complete'));
      ```
      
      ---
      
      ## Blade Components
      
      ### Anonymous Components
      
      ```blade
      {{-- resources/views/components/alert.blade.php --}}
      @props(['type' => 'info', 'dismissible' => false])
      
      <div class="alert alert-{{ $type }} {{ $dismissible ? 'alert-dismissible' : '' }}">
          {{ $slot }}
          @if($dismissible)
              <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
          @endif
      </div>
      
      {{-- Usage --}}
      <x-alert type="danger" dismissible>
          Something went wrong.
      </x-alert>
      ```
      
      ### Named Slots
      
      ```blade
      {{-- resources/views/components/modal.blade.php --}}
      @props(['id', 'title'])
      
      <div id="{{ $id }}" class="modal">
          <div class="modal-header">
              <h5>{{ $title }}</h5>
          </div>
          <div class="modal-body">
              {{ $slot }}
          </div>
          <div class="modal-footer">
              {{ $footer ?? '' }}
          </div>
      </div>
      
      {{-- Usage --}}
      <x-modal id="confirm-delete" title="Confirm Delete">
          Are you sure you want to delete this item?
      
          <x-slot:footer>
              <button>Cancel</button>
              <button class="btn-danger">Delete</button>
          </x-slot:footer>
      </x-modal>
      ```
      
      ### Class-Based Components
      
      ```php
      // php artisan make:component UserCard
      class UserCard extends Component
      {
          public readonly string $initials;
      
          public function __construct(
              public readonly User $user,
              public bool $showEmail = false,
          ) {
              $this->initials = strtoupper(
                  substr($user->first_name, 0, 1) . substr($user->last_name, 0, 1)
              );
          }
      
          public function render(): View
          {
              return view('components.user-card');
          }
      }
      
      {{-- resources/views/components/user-card.blade.php --}}
      <div class="user-card">
          <div class="avatar">{{ $initials }}</div>
          <h3>{{ $user->name }}</h3>
          @if($showEmail)
              <p>{{ $user->email }}</p>
          @endif
      </div>
      ```
      
      ### Stacks and Sections
      
      ```blade
      {{-- layout.blade.php --}}
      <html>
          <head>
              @stack('styles')       {{-- filled by child views --}}
          </head>
          <body>
              @yield('content')
              @stack('scripts')
          </body>
      </html>
      
      {{-- child.blade.php --}}
      @extends('layout')
      
      @push('styles')
          <link rel="stylesheet" href="/css/dashboard.css">
      @endpush
      
      @section('content')
          <h1>Dashboard</h1>
      @endsection
      
      @push('scripts')
          <script src="/js/dashboard.js"></script>
      @endpush
      ```
      
      ---
      
      ## Livewire Integration
      
      Livewire 3 handles server-side state with automatic DOM diffing.
      
      ### Component Structure
      
      ```php
      // php artisan make:livewire SearchUsers
      
      use Livewire\Attributes\{Computed, Url};
      use Livewire\Component;
      
      class SearchUsers extends Component
      {
          #[Url]                          // syncs to query string
          public string $search = '';
      
          public string $sortBy = 'name';
          public bool $showModal = false;
      
          // Runs when $search changes (debounced in view)
          public function updatedSearch(): void
          {
              $this->resetPage();
          }
      
          // Computed property (cached per render)
          #[Computed]
          public function users(): LengthAwarePaginator
          {
              return User::where('name', 'like', "%{$this->search}%")
                         ->orderBy($this->sortBy)
                         ->paginate(10);
          }
      
          public function deleteUser(int $userId): void
          {
              $this->authorize('delete', User::find($userId));
              User::destroy($userId);
              $this->dispatch('user-deleted'); // JS event
          }
      
          public function render(): View
          {
              return view('livewire.search-users');
          }
      }
      ```
      
      ```blade
      {{-- resources/views/livewire/search-users.blade.php --}}
      <div>
          <input wire:model.live.debounce.300ms="search" placeholder="Search...">
      
          <select wire:model.live="sortBy">
              <option value="name">Name</option>
              <option value="created_at">Newest</option>
          </select>
      
          @foreach($this->users as $user)
              <div wire:key="{{ $user->id }}">
                  {{ $user->name }}
                  <button wire:click="deleteUser({{ $user->id }})"
                          wire:confirm="Are you sure?">
                      Delete
                  </button>
              </div>
          @endforeach
      
          {{ $this->users->links() }}
      
          {{-- Lazy loading --}}
          <livewire:heavy-chart lazy />
      </div>
      ```
      
      ### Livewire File Uploads
      
      ```php
      use Livewire\WithFileUploads;
      
      class UploadAvatar extends Component
      {
          use WithFileUploads;
      
          #[Validate('image|max:1024')]
          public $photo;
      
          public function save(): void
          {
              $path = $this->photo->store('avatars', 's3');
              auth()->user()->update(['avatar' => $path]);
          }
      }
      ```
      
      ---
      
      ## Inertia.js
      
      Server-side routing + client-side rendering without a separate API.
      
      ### Laravel Side
      
      ```php
      // Controller returns Inertia response
      class PostController extends Controller
      {
          public function index(): Response
          {
              return Inertia::render('Posts/Index', [
                  'posts'  => PostResource::collection(Post::paginate(15)),
                  'filters' => request()->only(['search', 'status']),
              ]);
          }
      
          // Lazy-loaded props (only sent when explicitly requested)
          public function show(Post $post): Response
          {
              return Inertia::render('Posts/Show', [
                  'post'     => PostResource::make($post),
                  'comments' => Inertia::lazy(fn() => CommentResource::collection($post->comments()->paginate(20))),
              ]);
          }
      
          // Redirect after form submission
          public function store(StorePostRequest $request): RedirectResponse
          {
              $post = Post::create($request->validated() + ['user_id' => auth()->id()]);
              return redirect()->route('posts.show', $post)->with('success', 'Post created.');
          }
      }
      
      // Shared data (available on every page)
      // HandleInertiaRequests middleware
      public function share(Request $request): array
      {
          return [
              ...parent::share($request),
              'auth' => [
                  'user' => $request->user()?->only('id', 'name', 'email'),
              ],
              'flash' => [
                  'success' => $request->session()->get('success'),
                  'error'   => $request->session()->get('error'),
              ],
          ];
      }
      ```
      
      ### Vue Side (Inertia + Vue 3)
      
      ```vue
      <!-- resources/js/Pages/Posts/Index.vue -->
      <script setup>
      import { ref } from 'vue'
      import { router, useForm, usePage } from '@inertiajs/vue3'
      
      const props = defineProps({
          posts: Object,
          filters: Object,
      })
      
      const page = usePage()
      const auth = page.props.auth  // shared data
      
      // Form helper
      const form = useForm({
          title: '',
          body: '',
      })
      
      function submit() {
          form.post('/posts', {
              onSuccess: () => form.reset(),
          })
      }
      
      // Partial reloads (only refresh 'posts' prop)
      function search(query) {
          router.get('/posts', { search: query }, {
              preserveState: true,
              only: ['posts'],
          })
      }
      </script>
      
      <template>
          <div>
              <div v-for="post in posts.data" :key="post.id">
                  <Link :href="`/posts/${post.id}`">{{ post.title }}</Link>
              </div>
      
              <!-- Inertia pagination links -->
              <Pagination :links="posts.links" />
          </div>
      </template>
      ```
      
      ---
      
      ## Blade Directives and Helpers
      
      ### Authorization Directives
      
      ```blade
      @auth
          <a href="/dashboard">Dashboard</a>
      @endauth
      
      @guest
          <a href="/login">Login</a>
      @endguest
      
      @can('update', $post)
          <a href="{{ route('posts.edit', $post) }}">Edit</a>
      @endcan
      
      @cannot('delete', $post)
          <p>You cannot delete this post.</p>
      @endcannot
      
      @role('admin')   {{-- if using spatie/laravel-permission --}}
          <a href="/admin">Admin Panel</a>
      @endrole
      ```
      
      ### Looping Directives
      
      ```blade
      @forelse($posts as $post)
          <article>{{ $post->title }}</article>
      @empty
          <p>No posts found.</p>
      @endforelse
      
      {{-- Loop variable --}}
      @foreach($items as $item)
          @if($loop->first) <ul> @endif
          <li class="{{ $loop->even ? 'even' : 'odd' }}">
              {{ $loop->iteration }}. {{ $item->name }}
          </li>
          @if($loop->last) </ul> @endif
      @endforeach
      ```
      
      ### Custom Directives
      
      ```php
      // AppServiceProvider::boot()
      Blade::directive('currency', function ($expression) {
          return "<?php echo '$' . number_format({$expression}, 2); ?>";
      });
      
      Blade::if('env', function (string $environment) {
          return app()->environment($environment);
      });
      // Usage: @env('production') ... @endenv
      ```
      
    • eloquent-queries.md 20.8 KB
      # Eloquent Queries Reference
      
      Deep-dive reference for Eloquent ORM: relationships, query builder, scopes, accessors, mutators, events, soft deletes, pagination, performance, collections, and factories.
      
      ---
      
      ## Relationships
      
      ### hasOne
      
      ```php
      // User hasOne Profile
      class User extends Model
      {
          public function profile(): HasOne
          {
              return $this->hasOne(Profile::class);
              // Convention: profiles.user_id
              // Custom: $this->hasOne(Profile::class, 'foreign_key', 'local_key')
          }
      }
      
      // Usage
      $profile = $user->profile;                    // lazy load
      $user = User::with('profile')->find(1);       // eager load
      $user->profile()->create(['bio' => '...']);   // create via relationship
      ```
      
      ### hasMany
      
      ```php
      class User extends Model
      {
          public function posts(): HasMany
          {
              return $this->hasMany(Post::class);
          }
      }
      
      // Usage
      $posts = $user->posts;                        // Collection
      $posts = $user->posts()->published()->get();  // chained query
      $user->posts()->createMany([
          ['title' => 'First'],
          ['title' => 'Second'],
      ]);
      ```
      
      ### belongsTo
      
      ```php
      class Post extends Model
      {
          public function author(): BelongsTo
          {
              return $this->belongsTo(User::class, 'user_id'); // explicit FK
          }
      }
      
      // Avoid null when accessing author before saving
      $post->author()->associate($user); // sets user_id
      $post->save();
      
      // Dissociate (set FK to null)
      $post->author()->dissociate();
      $post->save();
      ```
      
      ### belongsToMany (many-to-many with pivot)
      
      ```php
      class User extends Model
      {
          public function roles(): BelongsToMany
          {
              return $this->belongsToMany(Role::class)
                          ->withPivot('assigned_at', 'assigned_by')
                          ->withTimestamps()
                          ->using(RoleUser::class); // custom pivot model
          }
      }
      
      // Pivot model with extra attributes
      class RoleUser extends Pivot
      {
          protected $casts = [
              'assigned_at' => 'datetime',
          ];
      }
      
      // Attach / detach / sync
      $user->roles()->attach($roleId, ['assigned_by' => auth()->id()]);
      $user->roles()->detach($roleId);
      $user->roles()->sync([1, 2, 3]);                    // replaces all
      $user->roles()->syncWithoutDetaching([4, 5]);       // additive only
      $user->roles()->toggle([1, 2]);                     // attach if not, detach if yes
      
      // Querying pivot
      $user->roles()->wherePivot('assigned_by', $userId)->get();
      
      // Access pivot in result
      foreach ($user->roles as $role) {
          echo $role->pivot->assigned_at;
      }
      ```
      
      ### hasManyThrough
      
      ```php
      // Country → User → Post (access posts through users)
      class Country extends Model
      {
          public function posts(): HasManyThrough
          {
              return $this->hasManyThrough(
                  Post::class,  // final model
                  User::class,  // intermediate model
                  'country_id', // FK on users table
                  'user_id',    // FK on posts table
                  'id',         // local key on countries
                  'id'          // local key on users
              );
          }
      }
      ```
      
      ### hasOneThrough
      
      ```php
      // Mechanic → Car → CarOwner (through single intermediary)
      class Mechanic extends Model
      {
          public function carOwner(): HasOneThrough
          {
              return $this->hasOneThrough(Owner::class, Car::class);
          }
      }
      ```
      
      ### Polymorphic: morphTo / morphMany
      
      ```php
      // Comment can belong to Post or Video
      class Comment extends Model
      {
          public function commentable(): MorphTo
          {
              return $this->morphTo(); // uses commentable_type + commentable_id
          }
      }
      
      class Post extends Model
      {
          public function comments(): MorphMany
          {
              return $this->morphMany(Comment::class, 'commentable');
          }
      }
      
      class Video extends Model
      {
          public function comments(): MorphMany
          {
              return $this->morphMany(Comment::class, 'commentable');
          }
      }
      
      // Usage
      $post->comments()->create(['body' => 'Great post!']);
      $comment->commentable; // returns Post or Video instance
      
      // Morph map (cleaner DB values)
      // AppServiceProvider::boot()
      Relation::morphMap([
          'post'  => Post::class,
          'video' => Video::class,
      ]);
      ```
      
      ### morphToMany (polymorphic many-to-many)
      
      ```php
      // Post and Video can have many Tags
      class Post extends Model
      {
          public function tags(): MorphToMany
          {
              return $this->morphToMany(Tag::class, 'taggable');
              // pivot: taggables (taggable_id, taggable_type, tag_id)
          }
      }
      
      class Tag extends Model
      {
          public function posts(): MorphedByMany
          {
              return $this->morphedByMany(Post::class, 'taggable');
          }
      }
      ```
      
      ---
      
      ## Query Builder
      
      ### Basic Constraints
      
      ```php
      // Where clauses
      User::where('status', 'active')
          ->where('age', '>=', 18)
          ->orWhere('is_admin', true)
          ->get();
      
      // whereIn / whereNotIn
      Post::whereIn('status', ['published', 'featured'])->get();
      Post::whereNotIn('user_id', [1, 2, 3])->get();
      
      // whereNull / whereNotNull
      User::whereNull('deleted_at')->get();
      User::whereNotNull('email_verified_at')->get();
      
      // whereBetween
      Order::whereBetween('total', [100, 500])->get();
      
      // whereDate / whereYear / whereMonth / whereDay
      Post::whereDate('created_at', '2024-01-15')->get();
      Post::whereYear('created_at', 2024)->get();
      
      // whereColumn (compare two columns)
      Order::whereColumn('shipped_at', '>', 'ordered_at')->get();
      ```
      
      ### Relationship Constraints
      
      ```php
      // whereHas: filter models with related models matching condition
      Post::whereHas('comments', function (Builder $query) {
          $query->where('approved', true);
      })->get();
      
      // whereDoesntHave
      Post::whereDoesntHave('comments')->get(); // posts with no comments
      
      // withWhereHas: eager load + constrain simultaneously
      Post::withWhereHas('comments', fn($q) => $q->approved())->get();
      
      // whereHas with count
      Post::whereHas('comments', fn($q) => $q, '>=', 5)->get(); // at least 5 comments
      ```
      
      ### Subqueries
      
      ```php
      // Select subquery
      $users = User::addSelect([
          'last_login_at' => Login::select('created_at')
              ->whereColumn('user_id', 'users.id')
              ->latest()
              ->limit(1),
      ])->get();
      
      // orderBy subquery
      $users = User::orderByDesc(
          Login::select('created_at')
              ->whereColumn('user_id', 'users.id')
              ->latest()
              ->limit(1)
      )->get();
      
      // From subquery
      $orders = DB::table(function (Builder $query) {
          $query->from('orders')->where('status', 'shipped');
      }, 'shipped_orders')->get();
      ```
      
      ### Raw Expressions
      
      ```php
      // selectRaw
      User::selectRaw('COUNT(*) as total, DATE(created_at) as date')
          ->groupByRaw('DATE(created_at)')
          ->get();
      
      // whereRaw
      User::whereRaw('LOWER(email) = ?', [strtolower($email)])->first();
      
      // orderByRaw
      Post::orderByRaw('FIELD(status, "featured", "published", "draft")')->get();
      
      // havingRaw
      User::selectRaw('country, COUNT(*) as total')
          ->groupBy('country')
          ->havingRaw('COUNT(*) > ?', [100])
          ->get();
      ```
      
      ---
      
      ## Query Scopes
      
      ### Local Scopes
      
      ```php
      class Post extends Model
      {
          // Constraint scope
          public function scopePublished(Builder $query): void
          {
              $query->where('status', 'published')
                    ->whereNotNull('published_at');
          }
      
          // Dynamic scope with parameter
          public function scopeByStatus(Builder $query, string $status): void
          {
              $query->where('status', $status);
          }
      
          // Scope with optional parameter
          public function scopeRecent(Builder $query, int $days = 7): void
          {
              $query->where('created_at', '>=', now()->subDays($days));
          }
      }
      
      // Usage (chaining scopes)
      Post::published()->recent(30)->orderByDesc('published_at')->paginate(15);
      Post::byStatus('draft')->get();
      ```
      
      ### Global Scopes
      
      ```php
      // Define scope class
      class ActiveScope implements Scope
      {
          public function apply(Builder $builder, Model $model): void
          {
              $builder->where('active', true);
          }
      }
      
      // Apply globally (in model boot or via attribute in Laravel 11+)
      class User extends Model
      {
          protected static function booted(): void
          {
              static::addGlobalScope(new ActiveScope());
              // Or anonymous: static::addGlobalScope('active', fn(Builder $b) => $b->where('active', true));
          }
      }
      
      // Removing global scope for specific query
      User::withoutGlobalScope(ActiveScope::class)->get();
      User::withoutGlobalScope('active')->get();
      User::withoutGlobalScopes()->get(); // remove all
      ```
      
      ---
      
      ## Accessors and Mutators (Laravel 11+ Attribute Class)
      
      ```php
      use Illuminate\Database\Eloquent\Casts\Attribute;
      
      class User extends Model
      {
          // Accessor only
          protected function fullName(): Attribute
          {
              return Attribute::make(
                  get: fn() => "{$this->first_name} {$this->last_name}",
              );
          }
      
          // Mutator only
          protected function password(): Attribute
          {
              return Attribute::make(
                  set: fn(string $value) => bcrypt($value),
              );
          }
      
          // Accessor + Mutator
          protected function name(): Attribute
          {
              return Attribute::make(
                  get: fn(string $value) => ucfirst($value),
                  set: fn(string $value) => strtolower($value),
              )->withoutObjectCaching(); // recompute each access
          }
      }
      
      // Usage
      $user->full_name;       // "John Doe"
      $user->password = 'secret'; // automatically hashed
      ```
      
      ### Built-in Casts
      
      ```php
      protected $casts = [
          'is_admin'       => 'boolean',
          'score'          => 'float',
          'metadata'       => 'array',          // JSON column ↔ array
          'preferences'    => 'collection',      // JSON ↔ Collection
          'settings'       => AsArrayObject::class,    // JSON ↔ ArrayObject (mutable)
          'options'        => AsCollection::class,     // JSON ↔ Collection (mutable)
          'secret'         => 'encrypted',       // transparent encryption
          'secret_array'   => 'encrypted:array', // encrypted JSON
          'birthday'       => 'date',            // Carbon without time
          'published_at'   => 'datetime',        // Carbon with time
          'status'         => PostStatus::class, // PHP 8.1 enum
      ];
      ```
      
      ### Enum Casting (PHP 8.1+)
      
      ```php
      enum PostStatus: string
      {
          case Draft     = 'draft';
          case Published = 'published';
          case Archived  = 'archived';
      }
      
      class Post extends Model
      {
          protected $casts = [
              'status' => PostStatus::class,
          ];
      }
      
      // Usage
      $post->status = PostStatus::Published; // or 'published'
      $post->status->label();                // if you add methods to enum
      Post::where('status', PostStatus::Published)->get();
      ```
      
      ---
      
      ## Eloquent Events
      
      ### Model Lifecycle Events
      
      | Event | Fires When |
      |-------|-----------|
      | `creating` | Before INSERT (can cancel with false) |
      | `created` | After INSERT |
      | `updating` | Before UPDATE (can cancel with false) |
      | `updated` | After UPDATE |
      | `saving` | Before INSERT or UPDATE |
      | `saved` | After INSERT or UPDATE |
      | `deleting` | Before DELETE (can cancel with false) |
      | `deleted` | After DELETE |
      | `restoring` | Before restore (soft delete) |
      | `restored` | After restore |
      | `retrieved` | After SELECT (heavy use discouraged) |
      
      ### Registering Listeners
      
      ```php
      // Option 1: $dispatchesEvents on model
      class Post extends Model
      {
          protected $dispatchesEvents = [
              'created'  => PostCreated::class,
              'deleted'  => PostDeleted::class,
          ];
      }
      
      // Option 2: boot() method (for closures)
      class Post extends Model
      {
          protected static function booted(): void
          {
              static::creating(function (Post $post) {
                  $post->slug = Str::slug($post->title);
              });
      
              static::deleting(function (Post $post) {
                  $post->comments()->delete(); // cascade via Eloquent
              });
          }
      }
      ```
      
      ### Observer Classes
      
      ```php
      // php artisan make:observer PostObserver --model=Post
      
      class PostObserver
      {
          public function creating(Post $post): void
          {
              $post->slug = Str::slug($post->title);
              $post->user_id ??= auth()->id();
          }
      
          public function created(Post $post): void
          {
              Cache::tags('posts')->flush();
          }
      
          public function updated(Post $post): void
          {
              Cache::tags('posts')->flush();
          }
      
          public function deleted(Post $post): void
          {
              $post->comments()->delete();
          }
      }
      
      // Register in AppServiceProvider::boot()
      Post::observe(PostObserver::class);
      
      // Silence observer for bulk operations
      Post::withoutObservers(function () {
          Post::query()->update(['featured' => false]);
      });
      ```
      
      ---
      
      ## Soft Deletes
      
      ```php
      use Illuminate\Database\Eloquent\SoftDeletes;
      
      class Post extends Model
      {
          use SoftDeletes; // adds deleted_at column
      }
      
      // Migration
      Schema::table('posts', function (Blueprint $table) {
          $table->softDeletes(); // nullable deleted_at timestamp
      });
      
      // Usage
      $post->delete();           // sets deleted_at (soft delete)
      $post->forceDelete();      // permanent DELETE
      
      // Querying
      Post::all();               // excludes soft-deleted (default)
      Post::withTrashed()->get(); // includes soft-deleted
      Post::onlyTrashed()->get(); // only soft-deleted
      
      // Restore
      Post::withTrashed()->find($id)->restore();
      Post::withTrashed()->where('user_id', $userId)->restore();
      
      // Check state
      $post->trashed();          // bool
      
      // Route model binding includes soft-deleted
      Route::get('/posts/{post}', [PostController::class, 'show'])
          ->withTrashed();
      ```
      
      ---
      
      ## Pagination
      
      | Method | Returns | Use When |
      |--------|---------|----------|
      | `paginate(15)` | `LengthAwarePaginator` | Need total count and last page |
      | `simplePaginate(15)` | `Paginator` | Large datasets, just next/prev needed |
      | `cursorPaginate(15)` | `CursorPaginator` | Huge datasets, consistent performance |
      
      ```php
      // Standard pagination (requires COUNT query)
      $posts = Post::published()->paginate(15);
      // Blade: {{ $posts->links() }}
      
      // Simple pagination (no COUNT, just LIMIT+1)
      $posts = Post::published()->simplePaginate(15);
      
      // Cursor pagination (keyset pagination - best for infinite scroll)
      $posts = Post::orderBy('id')->cursorPaginate(15);
      // URL: /posts?cursor=eyJpZCI6MTAwfQ
      
      // JSON API response
      return PostResource::collection($posts); // preserves pagination meta
      
      // Manual pagination
      $total = Post::count();
      $posts = Post::skip($offset)->take($perPage)->get();
      $paginator = new LengthAwarePaginator($posts, $total, $perPage, $currentPage);
      ```
      
      ---
      
      ## Performance: Chunking and Lazy Loading
      
      ### When to Use Each
      
      | Method | Memory | Speed | Use When |
      |--------|--------|-------|----------|
      | `get()` | All records | Fast | < 10k records |
      | `chunk(1000)` | Chunk size | Moderate | Large datasets, mutations |
      | `chunkById(1000)` | Chunk size | More stable | Large datasets (avoids offset drift) |
      | `lazy()` | Low (generator) | Fast | Read-only iteration |
      | `cursor()` | Very low | Fastest | Streaming large result sets |
      
      ```php
      // chunk - runs separate queries per chunk
      Post::where('status', 'draft')->chunk(500, function (Collection $posts) {
          foreach ($posts as $post) {
              $post->update(['status' => 'published']);
          }
      });
      
      // chunkById - stable cursor-based chunking (avoids missing rows when deleting)
      Post::orderBy('id')->chunkById(500, function (Collection $posts) {
          $posts->each->delete();
      });
      
      // lazy - PHP generator, single query with cursor
      foreach (Post::lazy(500) as $post) {
          ProcessPost::dispatch($post);
      }
      
      // cursor - yields one model at a time, minimal memory
      foreach (Post::cursor() as $post) {
          echo $post->title . PHP_EOL;
      }
      ```
      
      ### Query Logging and Debugging
      
      ```php
      // Log all queries (AppServiceProvider::boot)
      DB::listen(function (QueryExecuted $query) {
          Log::channel('queries')->info($query->sql, [
              'bindings' => $query->bindings,
              'time'     => $query->time,
          ]);
      });
      
      // Explain a query
      $posts = Post::with('comments')->where('status', 'published');
      dd($posts->explain()); // EXPLAIN output
      
      // Count queries executed (testing)
      DB::enableQueryLog();
      // ... run code ...
      $queries = DB::getQueryLog();
      expect($queries)->toHaveCount(2); // assert no N+1
      DB::disableQueryLog();
      
      // Prevent lazy loading in development
      Model::preventLazyLoading(! app()->isProduction());
      ```
      
      ---
      
      ## Collections
      
      Eloquent returns `Illuminate\Database\Eloquent\Collection` (extends base Collection).
      
      ```php
      $users = User::all();
      
      // Transformation
      $names      = $users->pluck('name');                    // Collection of names
      $names      = $users->pluck('name', 'id');              // ['id' => 'name'] keyed
      $active     = $users->filter(fn($u) => $u->is_active);
      $admins     = $users->where('role', 'admin');
      $mapped     = $users->map(fn($u) => ['id' => $u->id, 'email' => $u->email]);
      $grouped    = $users->groupBy('country');               // keyed Collection of Collections
      $sorted     = $users->sortBy('name');
      $sorted     = $users->sortByDesc(fn($u) => $u->posts_count);
      
      // Aggregation
      $total      = $users->sum('balance');
      $avg        = $users->avg('score');
      $max        = $users->max('score');
      $count      = $users->count();
      $first      = $users->first(fn($u) => $u->is_admin);
      
      // Unique / diff / intersect
      $unique     = $users->unique('email');
      $diff       = $users->diff($otherUsers);
      
      // Collection to array/JSON
      $array      = $users->toArray();
      $json       = $users->toJson();
      
      // Reduce
      $total = $users->reduce(fn($carry, $user) => $carry + $user->balance, 0);
      
      // Eloquent-specific collection methods
      $users->find(1);                                        // find by PK
      $users->load('posts');                                  // eager load on collection
      $users->modelKeys();                                    // array of primary keys
      $users->contains($user);                                // check membership
      $users->diff($otherUsers);                              // by PK comparison
      
      // Lazy collections (memory efficient)
      User::lazy()->filter(fn($u) => $u->is_active)->each(fn($u) => ProcessUser::dispatch($u));
      ```
      
      ---
      
      ## Factories
      
      ```php
      // database/factories/PostFactory.php
      class PostFactory extends Factory
      {
          protected $model = Post::class;
      
          public function definition(): array
          {
              return [
                  'user_id'      => User::factory(),         // auto-create related
                  'title'        => $this->faker->sentence(),
                  'slug'         => $this->faker->unique()->slug(),
                  'body'         => $this->faker->paragraphs(3, true),
                  'status'       => 'published',
                  'published_at' => $this->faker->dateTimeBetween('-1 year'),
              ];
          }
      
          // States - modifiers
          public function draft(): static
          {
              return $this->state(['status' => 'draft', 'published_at' => null]);
          }
      
          public function featured(): static
          {
              return $this->state(['status' => 'featured']);
          }
      
          public function withTags(int $count = 3): static
          {
              return $this->afterCreating(function (Post $post) use ($count) {
                  $post->tags()->attach(Tag::factory()->count($count)->create());
              });
          }
      
          // Sequence - vary per-record
          public function configure(): static
          {
              return $this->sequence(
                  ['status' => 'draft'],
                  ['status' => 'published'],
                  ['status' => 'archived'],
              );
          }
      }
      
      // Usage in tests or seeders
      Post::factory()->create();                              // single
      Post::factory()->count(10)->create();                   // 10 records
      Post::factory()->draft()->create();                     // apply state
      Post::factory()->featured()->withTags(5)->create();     // chain states
      Post::factory()->for(User::factory()->admin())->create(); // explicit relationship
      Post::factory()->has(Comment::factory()->count(3))->create(); // hasMany
      Post::factory()->hasComments(3)->create();              // magic has method
      
      // In-memory (not persisted)
      Post::factory()->make();
      Post::factory()->makeMany(5);
      
      // Sequences
      Post::factory()->count(3)->sequence(
          ['status' => 'draft'],
          ['status' => 'published'],
          ['status' => 'archived'],
      )->create();
      
      // afterCreating callback
      Post::factory()->afterCreating(function (Post $post) {
          $post->searchIndex()->create(['content' => $post->body]);
      })->create();
      ```
      
      ---
      
      ## Advanced Patterns
      
      ### Subquery Selects for Aggregates (avoid N+1)
      
      ```php
      // Instead of: $users->each(fn($u) => $u->posts->count())
      // Do this:
      $users = User::addSelect([
          'posts_count' => Post::selectRaw('COUNT(*)')
              ->whereColumn('user_id', 'users.id'),
          'last_post_at' => Post::select('created_at')
              ->whereColumn('user_id', 'users.id')
              ->latest()
              ->limit(1),
      ])->get();
      ```
      
      ### Upsert
      
      ```php
      // Single upsert
      User::updateOrCreate(
          ['email' => 'user@example.com'],             // find by
          ['name' => 'John', 'role' => 'admin']        // update/create with
      );
      
      // Bulk upsert (one query)
      Post::upsert(
          [
              ['id' => 1, 'title' => 'Updated', 'slug' => 'updated'],
              ['id' => 2, 'title' => 'New Post', 'slug' => 'new-post'],
          ],
          uniqueBy: ['slug'],                          // conflict column(s)
          update: ['title']                            // columns to update on conflict
      );
      ```
      
      ### Locking for Concurrency
      
      ```php
      // Shared lock (read lock - prevent other writes)
      $order = Order::where('id', $id)->sharedLock()->first();
      
      // Exclusive lock (write lock - prevent other reads and writes)
      DB::transaction(function () use ($orderId) {
          $order = Order::where('id', $orderId)->lockForUpdate()->first();
          $order->decrement('quantity');
      });
      ```
      
    • testing-auth.md 27.1 KB
      # Testing and Authentication Reference
      
      Deep-dive reference for PHPUnit/Pest testing, Sanctum, Fortify, policies, form requests, and browser testing with Dusk.
      
      ---
      
      ## PHPUnit Setup
      
      ### phpunit.xml
      
      ```xml
      <?xml version="1.0" encoding="UTF-8"?>
      <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
               bootstrap="vendor/autoload.php"
               colors="true">
          <testsuites>
              <testsuite name="Unit">
                  <directory suffix="Test.php">./tests/Unit</directory>
              </testsuite>
              <testsuite name="Feature">
                  <directory suffix="Test.php">./tests/Feature</directory>
              </testsuite>
          </testsuites>
      
          <source>
              <include>
                  <directory suffix=".php">./app</directory>
              </include>
          </source>
      
          <php>
              <env name="APP_ENV" value="testing"/>
              <env name="APP_KEY" value="base64:test-key-32-chars-here-padded"/>
              <env name="CACHE_STORE" value="array"/>
              <env name="DB_CONNECTION" value="sqlite"/>
              <env name="DB_DATABASE" value=":memory:"/>
              <env name="MAIL_MAILER" value="array"/>
              <env name="QUEUE_CONNECTION" value="sync"/>
              <env name="SESSION_DRIVER" value="array"/>
          </php>
      </phpunit>
      ```
      
      ### Test Databases
      
      ```php
      // Option 1: SQLite in-memory (fastest)
      // .env.testing
      DB_CONNECTION=sqlite
      DB_DATABASE=:memory:
      
      // Option 2: Separate MySQL test database
      DB_CONNECTION=mysql
      DB_DATABASE=app_testing
      
      // Option 3: Per-test transaction rollback (fastest for MySQL)
      use Illuminate\Foundation\Testing\DatabaseTransactions;
      
      // Option 4: Migrate fresh per test class (safest, slowest)
      use Illuminate\Foundation\Testing\RefreshDatabase;
      ```
      
      ---
      
      ## Pest PHP (Preferred in Laravel 11+)
      
      ### Project Setup
      
      ```bash
      composer require pestphp/pest pestphp/pest-plugin-laravel --dev
      php artisan pest:install
      ```
      
      ### File Structure and Syntax
      
      ```php
      // tests/Feature/PostTest.php
      use App\Models\{Post, User};
      use Illuminate\Foundation\Testing\RefreshDatabase;
      
      uses(RefreshDatabase::class);
      
      // Group related tests
      describe('Post creation', function () {
          beforeEach(function () {
              $this->user = User::factory()->create();
              $this->actingAs($this->user);
          });
      
          it('creates a post with valid data', function () {
              $response = $this->post('/posts', [
                  'title' => 'My First Post',
                  'body'  => 'Post content here.',
              ]);
      
              $response->assertRedirect();
              $this->assertDatabaseHas('posts', ['title' => 'My First Post']);
          });
      
          it('requires a title', function () {
              $response = $this->post('/posts', ['body' => 'Content']);
              $response->assertInvalid(['title']);
          });
      
          it('is pending future implementation')->todo();
      });
      
      // Top-level tests
      test('guests cannot create posts', function () {
          $this->post('/posts', ['title' => 'Test'])->assertRedirect('/login');
      });
      ```
      
      ### Pest Expectations
      
      ```php
      // Chained expectations
      expect($value)
          ->toBeTrue()
          ->not->toBeNull()
          ->toEqual('expected')
          ->toBeString()
          ->toHaveCount(3)
          ->toContain('substring')
          ->toMatchArray(['key' => 'value'])
          ->toHaveKey('name')
          ->toHaveKeys(['id', 'name', 'email'])
          ->toBeBetween(1, 10)
          ->toBeGreaterThan(5)
          ->toBeLessThanOrEqual(100)
          ->toBeInstanceOf(User::class)
          ->toBeNull()
          ->toBeEmpty()
          ->toThrow(InvalidArgumentException::class, 'message');
      
      // Higher-order expectations
      expect([1, 2, 3])->each->toBeInt();
      expect($users)->each->toBeInstanceOf(User::class);
      
      // Expectations on collections
      expect($users)->sequence(
          fn($user) => $user->name->toBe('Alice'),
          fn($user) => $user->name->toBe('Bob'),
      );
      ```
      
      ### Datasets
      
      ```php
      it('validates email format', function (string $email, bool $valid) {
          $response = $this->post('/register', ['email' => $email]);
      
          if ($valid) {
              $response->assertValid(['email']);
          } else {
              $response->assertInvalid(['email']);
          }
      })->with([
          ['valid@example.com', true],
          ['not-an-email', false],
          ['missing@', false],
          ['@nodomain.com', false],
      ]);
      
      // Shared datasets
      // tests/Datasets/emails.php
      dataset('invalid_emails', ['not-email', '@nodomain', 'missing@tld']);
      ```
      
      ### Architectural Testing
      
      ```php
      // tests/Architecture/AppTest.php
      arch('controllers do not use Eloquent directly')
          ->expect('App\Http\Controllers')
          ->not->toUse(['Illuminate\Database\Eloquent\Model']);
      
      arch('actions are invokable')
          ->expect('App\Actions')
          ->toBeClasses()
          ->toHaveSuffix('Action');
      
      arch('models extend Eloquent')
          ->expect('App\Models')
          ->toExtend('Illuminate\Database\Eloquent\Model');
      
      arch('no debug functions in production code')
          ->expect('App')
          ->not->toUse(['dd', 'dump', 'ray', 'var_dump']);
      ```
      
      ---
      
      ## HTTP Tests
      
      ### Basic HTTP Testing
      
      ```php
      // GET requests
      $response = $this->get('/posts');
      $response = $this->getJson('/api/posts');           // sets Accept: application/json
      
      // POST / PUT / PATCH / DELETE
      $response = $this->post('/posts', $data);
      $response = $this->postJson('/api/posts', $data);
      $response = $this->put('/posts/1', $data);
      $response = $this->patch('/posts/1', ['status' => 'published']);
      $response = $this->delete('/posts/1');
      
      // With headers
      $response = $this->withHeaders(['X-Custom-Header' => 'value'])->get('/api/data');
      
      // With cookies
      $response = $this->withCookie('token', 'abc')->get('/dashboard');
      
      // Follow redirects
      $response = $this->followingRedirects()->post('/posts', $data);
      ```
      
      ### Response Assertions
      
      ```php
      // Status codes
      $response->assertOk();                                    // 200
      $response->assertCreated();                               // 201
      $response->assertAccepted();                              // 202
      $response->assertNoContent();                             // 204
      $response->assertMovedPermanently();                      // 301
      $response->assertFound();                                 // 302
      $response->assertNotModified();                           // 304
      $response->assertBadRequest();                            // 400
      $response->assertUnauthorized();                          // 401
      $response->assertPaymentRequired();                       // 402
      $response->assertForbidden();                             // 403
      $response->assertNotFound();                              // 404
      $response->assertMethodNotAllowed();                      // 405
      $response->assertUnprocessable();                         // 422
      $response->assertTooManyRequests();                       // 429
      $response->assertServerError();                           // 500
      $response->assertStatus(418);                             // custom
      
      // Redirect
      $response->assertRedirect('/home');
      $response->assertRedirectToRoute('dashboard');
      $response->assertRedirectContains('/orders');
      
      // View
      $response->assertViewIs('posts.index');
      $response->assertViewHas('posts');
      $response->assertViewHas('user', fn($user) => $user->id === 1);
      $response->assertSee('Hello World');
      $response->assertSeeText('Hello World');                  // strips HTML
      $response->assertDontSee('Error');
      
      // JSON
      $response->assertJson(['status' => 'ok', 'data' => ['id' => 1]]);
      $response->assertJsonFragment(['email' => 'user@example.com']);
      $response->assertJsonPath('data.user.name', 'John');
      $response->assertJsonPath('data.*.id', [1, 2, 3]);
      $response->assertJsonCount(3, 'data');
      $response->assertJsonStructure([
          'data' => [
              '*' => ['id', 'title', 'created_at'],
          ],
          'meta' => ['total', 'per_page'],
      ]);
      $response->assertJsonMissing(['password', 'remember_token']);
      $response->assertExactJson(['key' => 'value']);           // exact match
      
      // Headers and cookies
      $response->assertHeader('Content-Type', 'application/json');
      $response->assertCookie('session');
      $response->assertCookieMissing('auth_token');
      
      // Session
      $response->assertSessionHas('success');
      $response->assertSessionHasErrors(['email', 'password']);
      $response->assertSessionMissing('error');
      
      // Validation errors
      $response->assertValid(['name', 'email']);
      $response->assertInvalid(['email' => 'invalid email format']);
      ```
      
      ---
      
      ## Database Testing
      
      ### Traits
      
      ```php
      use Illuminate\Foundation\Testing\RefreshDatabase;
      // Migrates fresh for every test class (drops + re-migrates). Slower but safe.
      
      use Illuminate\Foundation\Testing\DatabaseTransactions;
      // Wraps each test in a transaction, rolls back. Fast, but doesn't work with external processes.
      
      use Illuminate\Foundation\Testing\DatabaseMigrations;
      // Migrates before the test suite, rolls back after. Per-file.
      ```
      
      ### Database Assertions
      
      ```php
      $this->assertDatabaseHas('users', [
          'email' => 'user@example.com',
          'role'  => 'admin',
      ]);
      
      $this->assertDatabaseMissing('users', [
          'email' => 'deleted@example.com',
      ]);
      
      $this->assertDatabaseCount('posts', 5);
      
      $this->assertSoftDeleted('posts', ['id' => $post->id]);
      $this->assertNotSoftDeleted('posts', ['id' => $post->id]);
      
      $this->assertDatabaseEmpty('cache');
      
      // Model-based assertions
      $this->assertModelExists($post);
      $this->assertModelMissing($deletedPost);
      ```
      
      ### Factory Usage in Tests
      
      ```php
      // Create persisted records
      $user = User::factory()->create();
      $user = User::factory()->admin()->create(['name' => 'Override Name']);
      
      // Create without persisting
      $user = User::factory()->make();
      
      // Create multiple
      $users = User::factory()->count(5)->create();
      
      // Create with relationships
      $post = Post::factory()
          ->for(User::factory()->admin())
          ->hasComments(3)
          ->withTags(5)
          ->create();
      
      // Seed specific data
      $this->seed(RoleSeeder::class);
      $this->seed([RoleSeeder::class, PermissionSeeder::class]);
      ```
      
      ---
      
      ## Mocking Facades
      
      ### Mail
      
      ```php
      Mail::fake();
      
      $this->post('/checkout', $orderData);
      
      Mail::assertSent(OrderConfirmationMail::class);
      Mail::assertSent(OrderConfirmationMail::class, 1);        // sent exactly once
      Mail::assertSent(OrderConfirmationMail::class, fn($mail) =>
          $mail->hasTo('customer@example.com') &&
          $mail->hasSubject('Your Order Confirmation')
      );
      Mail::assertNotSent(RefundMail::class);
      Mail::assertQueued(WeeklyNewsletterMail::class);           // queued, not sent
      Mail::assertNothingSent();
      ```
      
      ### Notification
      
      ```php
      Notification::fake();
      
      $this->post('/orders', $data);
      
      Notification::assertSentTo($user, InvoicePaidNotification::class);
      Notification::assertSentTo($user, InvoicePaidNotification::class, fn($n) =>
          $n->invoice->id === $invoiceId
      );
      Notification::assertNotSentTo($admin, InvoicePaidNotification::class);
      Notification::assertCount(2);
      Notification::assertNothingSent();
      
      // On-demand notifications
      Notification::assertSentOnDemand(AlertNotification::class, fn($n, $routes) =>
          $routes->hasRoute('mail', 'ops@example.com')
      );
      ```
      
      ### Event
      
      ```php
      Event::fake();
      // Or fake only specific events:
      Event::fake([OrderPlaced::class, PaymentProcessed::class]);
      
      $this->post('/orders', $data);
      
      Event::assertDispatched(OrderPlaced::class);
      Event::assertDispatched(OrderPlaced::class, fn($e) => $e->order->id === $orderId);
      Event::assertDispatchedTimes(StockUpdated::class, 3);
      Event::assertNotDispatched(OrderCancelled::class);
      Event::assertListening(OrderPlaced::class, SendOrderConfirmation::class);
      Event::assertNothingDispatched();
      ```
      
      ### Queue / Bus
      
      ```php
      Queue::fake();
      
      $this->post('/upload', $fileData);
      
      Queue::assertPushed(ProcessUpload::class);
      Queue::assertPushed(ProcessUpload::class, fn($job) => $job->filename === 'test.csv');
      Queue::assertPushedOn('imports', ProcessUpload::class);
      Queue::assertNotPushed(NotifyAdmin::class);
      Queue::assertCount(2);
      Queue::assertNothingPushed();
      
      // Bus for batches and chains
      Bus::fake();
      Bus::assertChained([ValidateData::class, ProcessData::class, NotifyUser::class]);
      Bus::assertBatched(fn($batch) => $batch->jobs->count() === 100);
      ```
      
      ### HTTP Client
      
      ```php
      Http::fake([
          'api.stripe.com/v1/charges' => Http::response([
              'id'     => 'ch_123',
              'status' => 'succeeded',
          ], 200),
          'api.sendgrid.com/*' => Http::response(['message' => 'success'], 202),
          '*' => Http::response('Not mocked', 404),  // catch-all
      ]);
      
      // Simulate failure
      Http::fake(['api.stripe.com/*' => Http::response(['error' => 'declined'], 402)]);
      
      // Sequence of responses
      Http::fake([
          'api.example.com/*' => Http::sequence()
              ->push(['data' => []],  200)
              ->push(['data' => [1]], 200)
              ->pushStatus(429),      // rate limit on 3rd call
      ]);
      
      // Assert requests were made
      Http::assertSent(fn($request) =>
          $request->url() === 'https://api.stripe.com/v1/charges' &&
          $request['amount'] === 2000
      );
      Http::assertSentCount(3);
      Http::assertNotSent(fn($request) => str_contains($request->url(), 'sendgrid'));
      ```
      
      ### Storage
      
      ```php
      Storage::fake('s3');
      
      $this->post('/avatars', ['photo' => UploadedFile::fake()->image('photo.jpg')]);
      
      Storage::disk('s3')->assertExists('avatars/photo.jpg');
      Storage::disk('s3')->assertMissing('avatars/old.jpg');
      ```
      
      ---
      
      ## Sanctum Authentication
      
      ### API Token Authentication
      
      ```php
      // Installation
      composer require laravel/sanctum
      php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
      php artisan migrate
      
      // User model
      use Laravel\Sanctum\HasApiTokens;
      class User extends Authenticatable { use HasApiTokens; }
      
      // Issue token (login endpoint)
      $token = $user->createToken('mobile-app', ['orders:read', 'orders:write']);
      return response()->json(['token' => $token->plainTextToken]);
      
      // Check abilities
      $user->tokenCan('orders:read');   // bool
      $user->currentAccessToken();      // PersonalAccessToken model
      
      // Token expiration (config/sanctum.php)
      'expiration' => 60 * 24 * 7,      // 7 days in minutes
      
      // Revoke tokens
      $user->tokens()->delete();         // all tokens
      $user->currentAccessToken()->delete(); // current only
      ```
      
      ### Testing with Sanctum
      
      ```php
      use Laravel\Sanctum\Sanctum;
      
      // Authenticate as user (no real token needed)
      Sanctum::actingAs($user);
      Sanctum::actingAs($user, ['orders:read', 'orders:write']); // with abilities
      
      // Feature test examples
      it('returns orders for authenticated user', function () {
          Sanctum::actingAs(User::factory()->create(), ['orders:read']);
          Order::factory()->count(3)->for(auth()->user())->create();
      
          $this->getJson('/api/orders')
               ->assertOk()
               ->assertJsonCount(3, 'data');
      });
      
      it('rejects requests without valid token', function () {
          $this->getJson('/api/orders')->assertUnauthorized();
      });
      
      it('enforces token abilities', function () {
          Sanctum::actingAs(User::factory()->create(), ['orders:read']); // no write ability
          $this->postJson('/api/orders', $data)->assertForbidden();
      });
      ```
      
      ### SPA Authentication (Cookie-based)
      
      ```php
      // Frontend must first hit GET /sanctum/csrf-cookie
      // Then POST /login with credentials
      // Subsequent requests use session cookie + X-XSRF-TOKEN header
      
      // config/sanctum.php
      'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,localhost:3000')),
      
      // routes/api.php
      Route::middleware('auth:sanctum')->get('/user', fn(Request $request) => $request->user());
      
      // CORS (config/cors.php)
      'paths'             => ['api/*', 'sanctum/csrf-cookie'],
      'allowed_origins'   => ['http://localhost:3000'],
      'supports_credentials' => true,
      ```
      
      ---
      
      ## Fortify (Headless Authentication)
      
      ```bash
      composer require laravel/fortify
      php artisan vendor:publish --provider="Laravel\Fortify\FortifyServiceProvider"
      php artisan migrate
      ```
      
      ### Configuration
      
      ```php
      // config/fortify.php
      'features' => [
          Features::registration(),
          Features::resetPasswords(),
          Features::emailVerification(),
          Features::updateProfileInformation(),
          Features::updatePasswords(),
          Features::twoFactorAuthentication([
              'confirm'        => true,
              'confirmPassword' => true,
          ]),
      ],
      ```
      
      ### Customizing Actions
      
      ```php
      // app/Actions/Fortify/CreateNewUser.php
      class CreateNewUser implements CreatesNewUsers
      {
          public function create(array $input): User
          {
              Validator::make($input, [
                  'name'     => ['required', 'string', 'max:255'],
                  'email'    => ['required', 'email', 'unique:users'],
                  'password' => ['required', Password::defaults(), 'confirmed'],
              ])->validate();
      
              return DB::transaction(function () use ($input) {
                  $user = User::create([
                      'name'     => $input['name'],
                      'email'    => $input['email'],
                      'password' => Hash::make($input['password']),
                  ]);
                  $user->assignRole('user');                    // spatie/laravel-permission
                  event(new Registered($user));
                  return $user;
              });
          }
      }
      
      // FortifyServiceProvider::boot()
      Fortify::createUsersUsing(CreateNewUser::class);
      Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
      Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
      Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
      ```
      
      ---
      
      ## Policies and Gates
      
      ### Defining a Policy
      
      ```php
      // php artisan make:policy PostPolicy --model=Post
      class PostPolicy
      {
          // Gates receive user as first arg (nullable for guests)
          public function viewAny(?User $user): bool
          {
              return true; // anyone can list posts
          }
      
          public function view(?User $user, Post $post): bool
          {
              return $post->is_published || $user?->id === $post->user_id;
          }
      
          public function create(User $user): bool
          {
              return $user->hasVerifiedEmail();
          }
      
          public function update(User $user, Post $post): bool
          {
              return $user->id === $post->user_id || $user->isAdmin();
          }
      
          public function delete(User $user, Post $post): bool
          {
              return $user->id === $post->user_id || $user->isAdmin();
          }
      
          public function restore(User $user, Post $post): bool
          {
              return $user->isAdmin();
          }
      
          public function forceDelete(User $user, Post $post): bool
          {
              return $user->isAdmin();
          }
      }
      ```
      
      ### Registering Policies (Laravel 11+ auto-discovery)
      
      ```php
      // Auto-discovered if model/policy naming convention followed
      // OR manual registration in AppServiceProvider::boot():
      Gate::policy(Post::class, PostPolicy::class);
      ```
      
      ### Using Policies
      
      ```php
      // Controller
      class PostController extends Controller
      {
          public function update(Request $request, Post $post): RedirectResponse
          {
              $this->authorize('update', $post);
              // ...
          }
      
          // Resource controller - authorize all methods at once
          public function __construct()
          {
              $this->authorizeResource(Post::class, 'post');
          }
      }
      
      // Route-level middleware
      Route::put('/posts/{post}', [PostController::class, 'update'])
           ->middleware('can:update,post');
      
      // Blade
      @can('update', $post) ... @endcan
      @cannot('delete', $post) ... @endcannot
      
      // Manual check
      if (Gate::allows('update', $post)) { ... }
      if (Gate::denies('delete', $post)) { abort(403); }
      
      // Before all policy checks (super-admin bypass)
      Gate::before(fn(User $user) => $user->isSuperAdmin() ? true : null);
      ```
      
      ### Testing Policies
      
      ```php
      it('allows post author to update their post', function () {
          $user = User::factory()->create();
          $post = Post::factory()->for($user)->create();
      
          $this->actingAs($user)
               ->put("/posts/{$post->id}", ['title' => 'Updated'])
               ->assertOk();
      });
      
      it('prevents non-author from updating post', function () {
          $author  = User::factory()->create();
          $visitor = User::factory()->create();
          $post    = Post::factory()->for($author)->create();
      
          $this->actingAs($visitor)
               ->put("/posts/{$post->id}", ['title' => 'Hacked'])
               ->assertForbidden();
      });
      ```
      
      ---
      
      ## Form Requests
      
      ### Request Class
      
      ```php
      // php artisan make:request StorePostRequest
      class StorePostRequest extends FormRequest
      {
          // Who can make this request?
          public function authorize(): bool
          {
              return $this->user()->hasVerifiedEmail();
          }
      
          // Validation rules
          public function rules(): array
          {
              return [
                  'title'       => ['required', 'string', 'min:5', 'max:255'],
                  'body'        => ['required', 'string', 'min:50'],
                  'status'      => ['required', Rule::in(['draft', 'published'])],
                  'tags'        => ['nullable', 'array', 'max:5'],
                  'tags.*'      => ['integer', 'exists:tags,id'],
                  'image'       => ['nullable', 'image', 'max:2048', 'mimes:jpg,png,webp'],
                  'published_at' => ['nullable', 'date', 'after:now', Rule::requiredIf($this->status === 'published')],
              ];
          }
      
          // Transform input before validation
          public function prepareForValidation(): void
          {
              $this->merge([
                  'slug'   => Str::slug($this->title ?? ''),
                  'status' => $this->status ?? 'draft',
              ]);
          }
      
          // Custom error messages
          public function messages(): array
          {
              return [
                  'title.required' => 'A post title is required.',
                  'body.min'       => 'Posts must be at least 50 characters.',
              ];
          }
      
          // Custom attribute names in error messages
          public function attributes(): array
          {
              return [
                  'published_at' => 'publication date',
              ];
          }
      
          // After validation hook (complex cross-field validation)
          public function after(): array
          {
              return [
                  function (Validator $validator) {
                      if ($this->hasFile('image') && $this->status === 'draft') {
                          $validator->errors()->add('image', 'Images cannot be added to draft posts.');
                      }
                  },
              ];
          }
      
          // Safe data for controller use
          // $request->validated() - only validated fields
          // $request->safe()->only(['title', 'body']) - subset
          // $request->safe()->except(['tags']) - exclude
      }
      ```
      
      ### Testing Form Requests
      
      ```php
      it('creates a post with valid data', function () {
          $user = User::factory()->verified()->create();
      
          $this->actingAs($user)->postJson('/posts', [
              'title'  => 'A Valid Post Title',
              'body'   => str_repeat('a', 50), // meet min:50
              'status' => 'draft',
          ])->assertCreated();
      });
      
      it('requires a title', function () {
          $this->actingAs(User::factory()->verified()->create())
               ->postJson('/posts', ['body' => str_repeat('a', 50), 'status' => 'draft'])
               ->assertUnprocessable()
               ->assertJsonValidationErrors(['title']);
      });
      
      // Test the form request class directly (unit test)
      it('validates correctly', function () {
          $request = StorePostRequest::create('/posts', 'POST', [
              'title'  => 'Valid Title',
              'body'   => str_repeat('a', 50),
              'status' => 'draft',
          ]);
      
          $validator = Validator::make($request->all(), (new StorePostRequest)->rules());
          expect($validator->fails())->toBeFalse();
      });
      ```
      
      ---
      
      ## Middleware Testing
      
      ```php
      // Test route with middleware applied
      it('redirects unauthenticated users', function () {
          $this->get('/dashboard')->assertRedirect('/login');
      });
      
      // Test with middleware excluded
      it('processes request without auth in test', function () {
          $response = $this->withoutMiddleware(Authenticate::class)->get('/dashboard');
          $response->assertOk();
      });
      
      // Exclude all middleware
      $this->withoutMiddleware()->get('/dashboard');
      
      // Exclude CSRF for POST tests (alternative to using withHeaders)
      // Usually unnecessary if using postJson() or RefreshDatabase
      ```
      
      ---
      
      ## Browser Testing with Dusk
      
      ### Setup
      
      ```bash
      composer require laravel/dusk --dev
      php artisan dusk:install
      # Update APP_URL in .env.dusk.local
      # Start Chrome: php artisan dusk:chrome-driver
      # Run tests: php artisan dusk
      ```
      
      ### Test Structure
      
      ```php
      // tests/Browser/LoginTest.php
      use Laravel\Dusk\Browser;
      use Tests\DuskTestCase;
      
      class LoginTest extends DuskTestCase
      {
          public function test_user_can_login(): void
          {
              $user = User::factory()->create(['password' => Hash::make('password')]);
      
              $this->browse(function (Browser $browser) use ($user) {
                  $browser->visit('/login')
                          ->type('email', $user->email)
                          ->type('password', 'password')
                          ->press('Login')
                          ->assertPathIs('/dashboard')
                          ->assertSee('Welcome back');
              });
          }
      
          public function test_user_can_upload_avatar(): void
          {
              $user = User::factory()->create();
      
              $this->browse(function (Browser $browser) use ($user) {
                  $browser->loginAs($user)
                          ->visit('/settings/profile')
                          ->attach('avatar', __DIR__.'/../fixtures/avatar.jpg')
                          ->press('Save')
                          ->assertSee('Profile updated');
              });
          }
      }
      ```
      
      ### Dusk Selectors and Assertions
      
      ```php
      $browser
          ->visit('/posts')
          ->assertTitle('Posts - My App')
          ->assertSee('Latest Posts')
          ->assertDontSee('Error')
          ->click('@create-post-btn')             // dusk="create-post-btn" attribute
          ->pause(500)                            // ms - prefer waitFor instead
          ->waitFor('.modal', 5)                  // wait up to 5s
          ->waitForText('Post created')
          ->waitUntilMissing('.spinner')
          ->assertVisible('#post-form')
          ->assertMissing('.error-message')
          ->type('input[name=title]', 'My Post')
          ->select('select[name=status]', 'published')
          ->check('input[name=featured]')
          ->uncheck('input[name=notify]')
          ->radio('input[name=type]', 'article')
          ->screenshot('after-form-fill')         // saves to tests/Browser/screenshots/
          ->assertInputValue('title', 'My Post')
          ->assertChecked('featured')
          ->press('Submit')
          ->assertPathIs('/posts')
          ->assertRouteIs('posts.index');
      
      // JavaScript execution
      $browser->script('document.querySelector(".modal").remove()');
      $value = $browser->value('#hidden-input');
      
      // Multiple browsers (for real-time features)
      $this->browse(function (Browser $alice, Browser $bob) {
          $alice->loginAs($this->user)->visit('/chat');
          $bob->loginAs($this->otherUser)->visit('/chat')
              ->type('#message', 'Hello!')
              ->press('Send');
          $alice->waitForText('Hello!')->assertSee('Hello!');
      });
      ```
      
      ---
      
      ## Test Helpers and Utilities
      
      ### Custom Test Helpers
      
      ```php
      // tests/TestCase.php - add reusable methods
      abstract class TestCase extends BaseTestCase
      {
          protected function signIn(?User $user = null): User
          {
              $user ??= User::factory()->create();
              $this->actingAs($user);
              return $user;
          }
      
          protected function signInAsAdmin(): User
          {
              $admin = User::factory()->admin()->create();
              $this->actingAs($admin);
              return $admin;
          }
      
          protected function assertValidationError(TestResponse $response, string $field): void
          {
              $response->assertUnprocessable()
                       ->assertJsonValidationErrors([$field]);
          }
      }
      ```
      
      ### Parallel Testing
      
      ```bash
      # Run tests in parallel (requires brianium/paratest)
      composer require brianium/paratest --dev
      php artisan test --parallel
      php artisan test --parallel --processes=4
      ```
      
      ```php
      // Use separate test database per process
      // phpunit.xml: <env name="DB_DATABASE" value="app_testing_${TEST_TOKEN}"/>
      // Or configure in ParallelRunner
      ```
      
      ### Test-Specific Configuration
      
      ```php
      // .env.testing overrides
      MAIL_MAILER=array
      QUEUE_CONNECTION=sync
      CACHE_STORE=array
      SESSION_DRIVER=array
      
      // Per-test config override
      Config::set('mail.default', 'array');
      Config::set('queue.default', 'sync');
      
      // Freeze time (Carbon)
      $this->travelTo(now()->setDate(2024, 1, 15));
      $this->travelBack();
      Carbon::setTestNow('2024-01-15 12:00:00');
      Carbon::setTestNow();   // reset
      ```
      
  • scripts
    • .gitkeep 0 B · in bundle
  • SKILL.md 16.6 KB
    ---
    name: laravel-ops
    description: "Laravel framework patterns, Eloquent ORM, authentication, queues, and testing. Use for: laravel, eloquent, artisan, blade, php, sanctum, livewire, inertia, pest, phpunit, forge, vapor, queue, middleware, migration, factory, seeder."
    when_to_use: "Use when building Laravel 11+ apps — e.g. 'write an Eloquent query or migration', 'set up Sanctum auth', 'configure queues and jobs', 'test with Pest or PHPUnit'. Covers artisan, Blade, Livewire/Inertia, Forge/Vapor, factories, and seeders."
    license: MIT
    allowed-tools: "Read Write Bash"
    metadata:
      author: claude-mods
      related-skills: sql-ops, postgres-ops, testing-ops, docker-ops
    ---
    
    # Laravel Operations
    
    > Facts verified as of 2026-07.
    
    Authoritative reference for Laravel 11+ development: architecture decisions, Eloquent patterns, authentication strategies, queue configuration, and testing approaches.
    
    ---
    
    ## Architecture Decision Tree
    
    ```
    What type of application?
    │
    ├─ Full-stack web (HTML responses)
    │  ├─ Simple CRUD, small team → Monolith (Blade + Eloquent directly)
    │  │   └─ Use action classes for business logic over 20 lines
    │  ├─ Rich interactivity needed → Livewire (server-driven reactivity)
    │  │   └─ Add Alpine.js for client-side micro-interactions
    │  └─ SPA-like feel, React/Vue team → Inertia.js
    │      └─ Keep server-side routing, dump client-side routing overhead
    │
    ├─ API backend (JSON responses)
    │  ├─ Single consumer (mobile/SPA) → API-only with Sanctum SPA auth
    │  ├─ Multiple consumers / public → RESTful API with token auth
    │  └─ Complex graph queries → Consider GraphQL (lighthouse-php/lighthouse)
    │
    ├─ Large team / complex domain
    │  ├─ Domain-driven → Modular monolith (app/Modules/{Domain}/)
    │  │   ├─ Each module: Models, Actions, Events, Jobs, Http/
    │  │   └─ Shared: app/Shared/ for cross-cutting concerns
    │  └─ Independent deployability needed → Microservices
    │      └─ Use Laravel Octane for high-throughput services
    │
    └─ What business logic pattern?
       ├─ Simple CRUD, < 20 lines → Direct Eloquent in controller
       ├─ Reusable operation (create order, send invoice) → Action class
       │   └─ Single public handle() or execute() method
       ├─ Complex queries, multiple data sources → Repository pattern
       │   └─ Interface + Eloquent implementation (enables swapping)
       └─ Cross-cutting operations (audit, caching) → Service class
           └─ Inject via constructor, bind in ServiceProvider
    ```
    
    ### Action Class vs Repository vs Service
    
    | Pattern | Use When | Example |
    |---------|----------|---------|
    | Action class | Single, reusable business operation | `CreateOrderAction`, `SendInvoiceAction` |
    | Repository | Abstract data access, multiple sources | `OrderRepository` with `EloquentOrderRepository` |
    | Service | Orchestrate multiple actions/repos | `OrderService` combining payment + inventory |
    | Direct Eloquent | Simple CRUD, < 5 lines in controller | `User::create($data)` |
    
    ---
    
    ## Eloquent Quick Reference
    
    ### Relationships
    
    | Relationship | Method | Foreign Key Convention |
    |-------------|--------|----------------------|
    | `hasOne` | `return $this->hasOne(Profile::class)` | `profiles.user_id` |
    | `hasMany` | `return $this->hasMany(Post::class)` | `posts.user_id` |
    | `belongsTo` | `return $this->belongsTo(User::class)` | `posts.user_id` |
    | `belongsToMany` | `return $this->belongsToMany(Role::class)` | `role_user` pivot |
    | `hasManyThrough` | `return $this->hasManyThrough(Post::class, User::class)` | Country → User → Post |
    | `morphTo` | `return $this->morphTo()` | `{col}_type`, `{col}_id` |
    | `morphMany` | `return $this->morphMany(Comment::class, 'commentable')` | Polymorphic |
    | `morphToMany` | `return $this->morphToMany(Tag::class, 'taggable')` | Polymorphic pivot |
    
    ### Eager Loading
    
    ```php
    // Prevent N+1: always eager load in controllers
    $posts = Post::with(['author', 'comments.author', 'tags'])->paginate(15);
    
    // Conditional eager loading (load after retrieval)
    $user->load('posts.comments');
    $user->loadMissing('posts'); // only if not already loaded
    
    // Eager load counts (no SELECT *)
    $posts = Post::withCount('comments')->get();
    
    // Constrained eager loading
    $posts = Post::with(['comments' => fn($q) => $q->approved()->latest()])->get();
    ```
    
    ### Query Scopes
    
    ```php
    // Local scope (reusable query constraint)
    public function scopeActive(Builder $query): void
    {
        $query->where('status', 'active');
    }
    
    // Usage: User::active()->get()
    
    // Dynamic scope
    public function scopeOfType(Builder $query, string $type): void
    {
        $query->where('type', $type);
    }
    // Usage: User::ofType('admin')->get()
    ```
    
    ### Mass Assignment
    
    ```php
    // Fillable (allowlist - preferred)
    protected $fillable = ['name', 'email', 'password'];
    
    // Guarded (denylist - use [] only if you trust all input)
    protected $guarded = ['id', 'is_admin'];
    
    // Never set guarded = [] in production code
    ```
    
    ---
    
    ## Artisan Command Cheat Sheet
    
    | Command | Purpose | Common Options |
    |---------|---------|----------------|
    | `make:model Post -mfs` | Model + migration + factory + seeder | `-c` controller, `-r` resource |
    | `make:controller PostController -r` | Resource controller (7 methods) | `--api` skips create/edit |
    | `make:request StorePostRequest` | Form request for validation | |
    | `make:job ProcessPayment` | Queueable job class | `--sync` for sync job |
    | `make:event OrderPlaced` | Event class | |
    | `make:listener SendOrderConfirmation -e OrderPlaced` | Listener for event | `--queued` |
    | `make:notification InvoicePaid` | Notification class | |
    | `make:policy PostPolicy -m Post` | Policy with model | |
    | `make:middleware EnsureUserIsAdmin` | HTTP middleware | |
    | `make:command SendDailyReport` | Custom Artisan command | |
    | `migrate` | Run pending migrations | `--step` for individual |
    | `migrate:rollback` | Roll back last batch | `--step=5` |
    | `migrate:fresh --seed` | Drop all + re-migrate + seed | |
    | `db:seed` | Run all seeders | `--class=UserSeeder` |
    | `tinker` | REPL with app context | |
    | `route:list` | Show all routes | `--name=api` filter |
    | `route:cache` | Cache routes for production | |
    | `config:cache` | Cache config for production | |
    | `view:cache` | Pre-compile Blade templates | |
    | `optimize` | Run all cache commands | `optimize:clear` to reset |
    | `queue:work` | Process queue jobs | `--queue=high,default` |
    | `queue:listen` | Work + auto-reload on code change | |
    | `queue:failed` | List failed jobs | |
    | `queue:retry all` | Retry all failed jobs | |
    | `schedule:run` | Run due scheduled tasks | |
    | `schedule:work` | Run scheduler every minute (dev) | |
    | `key:generate` | Generate APP_KEY | |
    | `test` | Run PHPUnit/Pest tests | `--filter=UserTest` |
    | `test --parallel` | Run tests in parallel | `--processes=4` |
    | `vendor:publish` | Publish package assets/config | `--tag=config` |
    
    ---
    
    ## Authentication Decision Tree
    
    ```
    What do you need?
    │
    ├─ SPA (Vue/React) + Laravel API backend
    │  └─ Sanctum SPA authentication
    │     ├─ Cookie-based (same domain or subdomain)
    │     ├─ Csrf-cookie endpoint: GET /sanctum/csrf-cookie
    │     └─ No tokens in localStorage (XSS safe)
    │
    ├─ Mobile app or third-party API consumers
    │  └─ Sanctum API tokens (Bearer tokens)
    │     ├─ createToken($name, $abilities)
    │     ├─ Token abilities for fine-grained control
    │     └─ Token expiration with token:prune schedule
    │
    ├─ Traditional web app (server-rendered)
    │  ├─ Just need auth pages quickly → Breeze
    │  │   ├─ Minimal, educational, Blade or Inertia stack
    │  │   └─ Install: composer require laravel/breeze --dev
    │  ├─ Need teams, 2FA, profile management → Jetstream
    │  │   ├─ Livewire or Inertia stack
    │  │   └─ Install: composer require laravel/jetstream
    │  └─ Need headless auth (API + custom UI) → Fortify
    │      ├─ Actions in app/Actions/Fortify/
    │      └─ Customize: CreateNewUser, UpdateUserPassword
    │
    └─ Custom / enterprise
       ├─ LDAP/SAML → socialiteproviders/saml2
       ├─ OAuth social login → laravel/socialite
       └─ Custom guard → Implement Guard + UserProvider contracts
    ```
    
    ### Sanctum Quick Setup
    
    ```php
    // config/sanctum.php - stateful domains for SPA
    'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost')),
    
    // API token creation
    $token = $user->createToken('mobile-app', ['orders:read', 'orders:write']);
    return ['token' => $token->plainTextToken];
    
    // Check token ability
    Route::get('/orders', function (Request $request) {
        $request->user()->tokenCan('orders:read'); // bool
    });
    
    // Protect routes
    Route::middleware('auth:sanctum')->group(function () {
        // authenticated routes
    });
    ```
    
    ---
    
    ## Queue Decision Tree
    
    ```
    Queue driver selection:
    │
    ├─ Development / testing
    │  └─ sync driver (executes immediately, no worker needed)
    │     QUEUE_CONNECTION=sync
    │
    ├─ Small app, no Redis available
    │  └─ database driver
    │     ├─ php artisan queue:table && migrate
    │     ├─ Works fine for < 100 jobs/min
    │     └─ QUEUE_CONNECTION=database
    │
    ├─ Medium-high throughput, self-hosted
    │  └─ Redis driver (via predis or phpredis)
    │     ├─ QUEUE_CONNECTION=redis
    │     ├─ Laravel Horizon for monitoring
    │     └─ Supports priorities, pausing, metrics
    │
    └─ AWS infrastructure / massive scale
       └─ SQS driver
          ├─ QUEUE_CONNECTION=sqs
          ├─ Managed, auto-scaling
          └─ Use with Laravel Vapor for serverless
    ```
    
    ### Job Patterns
    
    ```php
    // Basic job dispatch
    ProcessPayment::dispatch($order);
    ProcessPayment::dispatch($order)->onQueue('payments')->delay(now()->addMinutes(5));
    
    // Chaining (sequential)
    Bus::chain([
        new ProcessPayment($order),
        new SendInvoice($order),
        new UpdateInventory($order),
    ])->dispatch();
    
    // Batching (parallel + callback)
    $batch = Bus::batch([
        new ImportRow($row1),
        new ImportRow($row2),
        new ImportRow($row3),
    ])->then(fn(Batch $batch) => ImportComplete::dispatch())
      ->catch(fn(Batch $batch, Throwable $e) => Log::error($e))
      ->dispatch();
    
    // Rate limiting (throttle to 5 per minute)
    public function middleware(): array
    {
        return [new RateLimited('payments')];
    }
    
    // Unique jobs (prevent duplicate processing)
    use Illuminate\Contracts\Queue\ShouldBeUnique;
    
    class ProcessPayment implements ShouldQueue, ShouldBeUnique
    {
        public string $uniqueId => $this->order->id;
        public int $uniqueFor = 3600; // seconds
    }
    
    // Retry configuration
    public int $tries = 3;
    public int $backoff = 60; // seconds between retries
    
    public function retryUntil(): DateTime
    {
        return now()->addHours(24);
    }
    ```
    
    ### Task Scheduling
    
    ```php
    // routes/console.php (Laravel 11+)
    Schedule::job(SendDailyReport::class)->dailyAt('08:00')->timezone('America/New_York');
    Schedule::command('backup:run')->daily()->runInBackground()->emailOutputOnFailure('ops@app.com');
    Schedule::call(fn() => Cache::flush())->weekly()->sundays()->at('00:00');
    
    // Prevent overlap (long-running tasks)
    Schedule::job(ProcessImport::class)->everyFiveMinutes()->withoutOverlapping();
    
    // Run on one server only (requires Redis/database cache driver)
    Schedule::job(SendNewsletters::class)->daily()->onOneServer();
    ```
    
    ---
    
    ## Testing Quick Reference
    
    ### Test Types
    
    | Type | Class extends | Database | Purpose |
    |------|--------------|----------|---------|
    | Feature test | `Tests\TestCase` | Yes (with trait) | HTTP endpoints, full stack |
    | Unit test | `PHPUnit\Framework\TestCase` | No | Pure logic, no app boot |
    | Browser test | `Laravel\Dusk\TestCase` | Yes | Real browser via ChromeDriver |
    
    ### Database Traits
    
    ```php
    use Illuminate\Foundation\Testing\RefreshDatabase;   // migrate fresh each test (slower)
    use Illuminate\Foundation\Testing\DatabaseTransactions; // rollback each test (faster)
    ```
    
    ### Pest Syntax (preferred in Laravel 11+)
    
    ```php
    describe('User authentication', function () {
        beforeEach(function () {
            $this->user = User::factory()->create();
        });
    
        it('allows login with valid credentials', function () {
            $response = $this->post('/login', [
                'email' => $this->user->email,
                'password' => 'password',
            ]);
    
            $response->assertRedirect('/dashboard');
            $this->assertAuthenticatedAs($this->user);
        });
    
        it('rejects invalid credentials')->todo();
    });
    ```
    
    ### Common Assertions
    
    ```php
    // HTTP response
    $response->assertStatus(200);
    $response->assertOk();             // 200
    $response->assertCreated();        // 201
    $response->assertNoContent();      // 204
    $response->assertUnauthorized();   // 401
    $response->assertForbidden();      // 403
    $response->assertNotFound();       // 404
    $response->assertRedirect('/home');
    
    // JSON responses
    $response->assertJson(['status' => 'ok']);
    $response->assertJsonPath('data.email', 'user@example.com');
    $response->assertJsonCount(3, 'data');
    $response->assertJsonStructure(['data' => ['id', 'name', 'email']]);
    $response->assertJsonMissing(['password']);
    
    // Database
    $this->assertDatabaseHas('users', ['email' => 'user@example.com']);
    $this->assertDatabaseMissing('users', ['email' => 'deleted@example.com']);
    $this->assertDatabaseCount('posts', 5);
    $this->assertSoftDeleted('posts', ['id' => $post->id]);
    ```
    
    ---
    
    ## Common Gotchas
    
    | Gotcha | Why | Fix |
    |--------|-----|-----|
    | N+1 queries on relationships | Eloquent lazy-loads by default | Use `with()` eager loading; enable `Model::preventLazyLoading()` in AppServiceProvider during development |
    | Mass assignment vulnerability | `$fillable = []` accepts all | Always define `$fillable`; never use `$guarded = []` in production |
    | `created_at` not updating on `update()` | Only `updated_at` auto-sets | Use `$model->touch()` or `timestamps = true` (default) |
    | Queue job fails on model serialization | Model state may change between dispatch and processing | Use `SerializesModels` trait; re-fetch from DB in `handle()` if needed |
    | Timezone mismatch in scheduled tasks | Server tz != app tz | Set `APP_TIMEZONE` in `.env`; use `->timezone()` on schedule entries |
    | Middleware order matters | Auth middleware must run before policies | Global → route group → route. Auth before throttle check or vice versa changes 401 vs 429 |
    | Route model binding skips soft-deleted records | `RouteServiceProvider` ignores `trashed()` | Extend binding: `Route::bind('post', fn($id) => Post::withTrashed()->findOrFail($id))` |
    | Service container binding not auto-resolved | Interface not bound to implementation | Register in `AppServiceProvider::register()`: `$this->app->bind(Interface::class, Implementation::class)` |
    | Migration foreign key order | Must create referenced table first | Run `migrate:fresh` to verify; use `Schema::disableForeignKeyConstraints()` in tests |
    | CSRF protection blocks API routes | `VerifyCsrfToken` runs on all web routes | Register API routes in `routes/api.php` (uses `api` middleware group without CSRF) |
    | `env()` returns null after caching | `config:cache` bakes env values | Always access env via `config()` helper in app code; only use `env()` in `config/` files |
    | Blade `@stack` renders in wrong order | `@push` must appear after `@stack` in execution | Use `@prepend` for scripts that must appear first |
    | Event listener not firing | Listener not registered or discovered | Check `EventServiceProvider::$listen`; or enable `Event::discover()` in Laravel 11 |
    
    ---
    
    ## Reference Files
    
    | File | Contents |
    |------|---------|
    | `references/eloquent-queries.md` | Deep-dive: relationships, query builder, scopes, accessors, mutators, events, soft deletes, pagination, performance, collections, factories |
    | `references/architecture.md` | Service container, providers, facades, middleware, events, notifications, jobs, scheduling, Blade components, Livewire, Inertia |
    | `references/testing-auth.md` | PHPUnit/Pest setup, HTTP tests, database testing, fakes, Sanctum, Fortify, policies, form requests, Dusk |
    
    ---
    
    ## See Also
    
    - `sql-ops` - Query optimization, indexing strategy, raw SQL patterns
    - `postgres-ops` - PostgreSQL-specific features, JSON columns, full-text search
    - `testing-ops` - General testing philosophy, TDD, CI integration
    - `docker-ops` - Containerizing Laravel apps, Docker Compose, production setup
    
    ### Key External Resources
    
    - [Laravel 11 Documentation](https://laravel.com/docs/11.x)
    - [Pest PHP](https://pestphp.com/)
    - [Laravel Horizon](https://laravel.com/docs/11.x/horizon) - Queue monitoring
    - [Laravel Telescope](https://laravel.com/docs/11.x/telescope) - Local debugging and request/query monitoring
    - [Laravel Octane](https://laravel.com/docs/11.x/octane) - High-performance serving
    - [Laravel Forge](https://forge.laravel.com/) - Server management
    - [Laravel Vapor](https://vapor.laravel.com/) - Serverless deployment
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related