laravel-ops

Installation
SKILL.md

Laravel Operations

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

Related skills
Installs
7
GitHub Stars
17
First Seen
Mar 22, 2026