Laravel Apps skill for Claude Code

Eloquent without N+1, jobs that survive retries. Claude builds Laravel features the way a senior Laravel developer would: strict mode on so lazy loading fails in tests, validation in Form Requests, per-object policy checks, API Resources instead of raw models, and queued jobs with timeouts, backoff and idempotent handlers. Each feature ships with Pest tests that cover the success, 422 and 403 paths while external calls stay faked.

Name
laravel-apps
Size
282 lines · ≈2,800 tokens
Files
1
License
MIT
Updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Builds and reviews Laravel 11, 12 and 13 applications - Eloquent models without N+1 queries or mass-assignment holes, safe migrations, Form Request validation, policies and gates, queued jobs that survive retries and deploys, the scheduler, Livewire and Inertia data exposure, and Pest feature tests with fakes. Use when writing Laravel controllers, models, jobs or migrations, upgrading between Laravel versions, debugging slow Eloquent pages or stuck queues, or adding tests to a Laravel codebase.

Try asking

  • Add an orders API with policies, validation and Pest tests to this Laravel app
  • This Laravel dashboard runs 400 queries per page. Find and fix the N+1s
  • Our invoice job sometimes sends twice. Review the job and queue config

Use it when

  • Writing Laravel controllers, models, migrations or jobs
  • An Eloquent page is slow or issues many repeated queries
  • Queued jobs run twice, time out, or see missing rows
  • Adding feature tests to a Laravel codebase
  • Upgrading a Laravel 11 or 12 app to a newer major

Skip it when

  • The question is PHP language features or static analysis setup — use php-modern
  • You are designing queue architecture across stacks — use background-jobs

How to check the result

php artisan test passes with strict mode on, including 403 tests for every policy-protected route, and Larastan and Pint report no new issues.

The common mistake it prevents

Leaving retry_after in config/queue.php shorter than a job's timeout, so a second worker picks up a job that is still running and the side effect happens twice.

~/.claude/skills/laravel-apps/SKILL.md

--- name: laravel-apps description: Builds and reviews Laravel 11, 12 and 13 applications - Eloquent models without N+1 queries or mass-assignment holes, safe migrations, Form… license: MIT ---

Done well means: every controller action validates through a Form Request and authorizes through a policy, no page lazy-loads relations in a loop, every queued job is safe to run twice, migrations are additive and reversible, and php artisan test exercises the real HTTP layer with external services faked.

Workflow

- [ ] 1. Inventory the version, skeleton style and conventions
- [ ] 2. Turn on strictness in non-production environments
- [ ] 3. Model data: migration, model, factory, policy together
- [ ] 4. Write the HTTP layer: route, Form Request, controller, resource
- [ ] 5. Move slow or retryable work into queued jobs
- [ ] 6. Expose data to Livewire/Inertia deliberately
- [ ] 7. Test with Pest feature tests and fakes
- [ ] 8. Run the verification loop

1. Inventory first

php artisan about                 # Laravel + PHP version, drivers, cache state
composer show laravel/framework | head -5
php artisan route:list --except-vendor
php artisan model:show Order      # columns, casts, relations, observers

Record: Laravel major (11, 12 or 13; 13 shipped March 2026, needs PHP 8.3+ and promised no breaking changes from 12), whether the app uses the slim skeleton (bootstrap/app.php configures middleware, exceptions and routing; no app/Http/Kernel.php) or the pre-11 layout, the frontend stack (Blade, Livewire, Inertia with React/Vue, or API-only with Sanctum), queue driver, and test framework (Pest or PHPUnit). Match the existing layout; do not migrate a pre-11 skeleton to the slim one as a side effect.

2. Strictness in development

In AppServiceProvider::boot():

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;

Model::shouldBeStrict(! $this->app->isProduction()); // lazy loading, silent discards, missing attributes throw
DB::prohibitDestructiveCommands($this->app->isProduction()); // blocks migrate:fresh, db:wipe in prod

shouldBeStrict turns N+1 queries into exceptions in tests and local dev, which is the cheapest N+1 detector available.

3. Data layer

php artisan make:model Order -mfp     # migration, factory, policy

Migrations:

  • Use foreignIdFor(User::class)->constrained()->cascadeOnDelete() (or restrictOnDelete) so the database enforces relationships.
  • Never edit a migration that has run in any shared environment; add a new one.
  • Adding a NOT NULL column to a large table: add it nullable, backfill in chunks (chunkById), then alter to NOT NULL in a later migration.
  • Preview SQL before running against shared databases: php artisan migrate --pretend.
  • migrate:fresh, db:wipe and migrate:reset destroy data. Run them only locally, and ask the user before running any destructive command against a non-local database.

Models:

class Order extends Model
{
    use HasFactory;

    protected $fillable = ['status', 'total_cents', 'currency']; // never user_id, is_admin

    protected function casts(): array
    {
        return [
            'status' => OrderStatus::class,     // backed enum
            'paid_at' => 'immutable_datetime',
            'meta' => 'array',
        ];
    }

    public function user(): BelongsTo { return $this->belongsTo(User::class); }
    public function items(): HasMany { return $this->hasMany(OrderItem::class); }
}
  • Prefer $fillable over $guarded = []. Set ownership fields explicitly: $request->user()->orders()->create($request->validated()).
  • Money in integer minor units, never floats.
  • Eager load at the query: Order::with(['items.product', 'user'])->withCount('items').
  • Iterate large sets with chunkById(500, ...) or lazyById(); chunk() skips rows when the loop updates the column it orders by.
  • Local scopes: Laravel 12+ also accepts the #[Scope] attribute on a protected method; the scopeX prefix still works. Laravel 13 adds PHP attribute alternatives for several model properties; property-based config keeps working, so follow the codebase.

4. HTTP layer

php artisan make:request StoreOrderRequest
php artisan make:resource OrderResource
class StoreOrderRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('create', Order::class);
    }

    public function rules(): array
    {
        return [
            'items' => ['required', 'array', 'min:1', 'max:100'],
            'items.*.product_id' => ['required', 'integer', 'exists:products,id'],
            'items.*.quantity' => ['required', 'integer', 'between:1,99'],
            'currency' => ['required', Rule::enum(Currency::class)],
        ];
    }
}

class OrderController extends Controller
{
    public function update(UpdateOrderRequest $request, Order $order): OrderResource
    {
        Gate::authorize('update', $order);           // per-object check
        $order->update($request->validated());
        return new OrderResource($order->load('items'));
    }
}
  • Since Laravel 11 the base Controller is empty: use Gate::authorize(), the can:update,order route middleware, or add the AuthorizesRequests trait yourself.
  • Never pass $request->all() to create/update. Use validated() or safe()->only().
  • Return API Resources, not models: resources stop hidden columns and new columns leaking.
  • Scope route model binding to the parent when nesting: Route::get('/teams/{team}/projects/{project}', ...)->scopeBindings();
  • API routes in the slim skeleton need php artisan install:api (adds routes/api.php and Sanctum).

5. Queues and scheduling

class SendInvoice implements ShouldQueue
{
    use Queueable;

    public int $tries = 5;
    public int $timeout = 60;            // seconds; worker kills the job after this
    public bool $deleteWhenMissingModels = true;

    public function __construct(public Order $order) {}

    public function backoff(): array { return [10, 60, 300]; }

    public function middleware(): array
    {
        return [(new WithoutOverlapping($this->order->id))->expireAfter(120)];
    }

    public function handle(InvoiceService $invoices): void
    {
        if ($this->order->invoice_sent_at) { return; }   // idempotent guard
        $invoices->send($this->order);
        $this->order->update(['invoice_sent_at' => now()]);
    }
}

Hard rules for jobs:

RuleWhy
retry_after in config/queue.php > the longest job $timeoutOtherwise a second worker starts the same job while the first still runs
Dispatch after commit ('after_commit' => true on the connection, or ->afterCommit())Job can run before the transaction commits and see no row
Pass IDs or models, never closures over request stateSerializesModels re-fetches the model; anything else is stale or unserializable
Make handle() idempotentEvery job can run more than once (timeouts, worker crashes, retry)
Run php artisan queue:restart on every deployLong-lived workers keep old code in memory
Use Horizon for Redis queues in productionPer-queue balancing, metrics, failed-job UI

Scheduler lives in routes/console.php on the slim skeleton:

Schedule::command('reports:daily')->dailyAt('02:00')->onOneServer()->withoutOverlapping();

onOneServer() requires a shared cache driver (Redis, database, memcached). Cron runs php artisan schedule:run every minute, or run php artisan schedule:work in a container.

6. Livewire and Inertia exposure

  • Livewire 3: public properties round-trip through the browser and can be tampered with. Mark IDs #[Locked], authorize inside every action method, and never store a model with sensitive attributes in a public property. wire:model is deferred by default; use wire:model.live only where per-keystroke requests are intended.
  • Inertia 2: every prop is serialized into the page HTML. Pass resources or ->only([...]) arrays, never whole models. Keep HandleInertiaRequests::share() small: it runs on every request. Use deferred or lazy props for expensive data.
  • Laravel 12+ starter kits (React, Vue, Livewire) are a reasonable base for new apps; do not retrofit them onto an existing app.

7. Tests with Pest

uses(RefreshDatabase::class);

it('lets an owner update their order', function () {
    $user = User::factory()->create();
    $order = Order::factory()->for($user)->create(['status' => OrderStatus::Pending]);

    $this->actingAs($user)
        ->patchJson("/api/orders/{$order->id}", ['status' => 'cancelled'])
        ->assertOk()
        ->assertJsonPath('data.status', 'cancelled');
});

it('forbids updating another user\'s order', function () {
    $order = Order::factory()->create();
    $this->actingAs(User::factory()->create())
        ->patchJson("/api/orders/{$order->id}", ['status' => 'cancelled'])
        ->assertForbidden();
});

it('queues the invoice after checkout', function () {
    Queue::fake();
    Http::preventStrayRequests();
    // ... perform checkout
    Queue::assertPushed(SendInvoice::class, fn ($job) => $job->order->is($order));
});
  • Fakes: Queue::fake(), Bus::fake(), Event::fake(), Mail::fake(), Notification::fake(), Storage::fake('s3'), Http::fake([...]). Add Http::preventStrayRequests() so an unfaked call fails the test.
  • Always include the forbidden-path test for each policy-protected route.
  • Pest 5 requires PHP 8.4 and Laravel 13 for its Laravel plugin; on older stacks stay on the Pest major the project already uses.

Gotchas

  • env() returns null outside config files once php artisan config:cache has run. Read settings through config('services.x.key').
  • $model->update() on a model loaded with select() of some columns silently works on partial data; strict mode surfaces missing attributes.
  • Observers and model events do not fire for mass updates (Order::where(...)->update()).
  • firstOrCreate is not atomic; back it with a unique index and catch UniqueConstraintViolationException, or use createOrFirst.
  • Octane keeps the app in memory: no request data in static properties or singletons.
  • APP_DEBUG=true in production exposes environment variables in error pages.

Verification

php artisan test --parallel           # or vendor/bin/pest --parallel
vendor/bin/phpstan analyse            # Larastan, level the project already uses
vendor/bin/pint --test                # code style without rewriting
php artisan migrate:fresh --seed      # LOCAL ONLY: migrations and seeders run clean
php artisan route:list --except-vendor

If a test fails with LazyLoadingViolationException, add the relation to the query’s with() rather than disabling strict mode. If a job runs twice in staging, compare retry_after with the job timeout before touching job code.

Output contract

For a feature, deliver: migration, model (casts, relations, fillable), factory, policy, Form Request, controller, resource, routes, and Pest tests covering success, validation failure (422) and forbidden (403). List any php artisan commands the user must run.

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc