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.
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()(orrestrictOnDelete) 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:wipeandmigrate:resetdestroy 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
$fillableover$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, ...)orlazyById();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; thescopeXprefix 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
Controlleris empty: useGate::authorize(), thecan:update,orderroute middleware, or add theAuthorizesRequeststrait yourself. - Never pass
$request->all()tocreate/update. Usevalidated()orsafe()->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(addsroutes/api.phpand 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:
| Rule | Why |
|---|---|
retry_after in config/queue.php > the longest job $timeout | Otherwise 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 state | SerializesModels re-fetches the model; anything else is stale or unserializable |
Make handle() idempotent | Every job can run more than once (timeouts, worker crashes, retry) |
Run php artisan queue:restart on every deploy | Long-lived workers keep old code in memory |
| Use Horizon for Redis queues in production | Per-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:modelis deferred by default; usewire:model.liveonly where per-keystroke requests are intended. - Inertia 2: every prop is serialized into the page HTML. Pass resources or
->only([...])arrays, never whole models. KeepHandleInertiaRequests::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([...]). AddHttp::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 oncephp artisan config:cachehas run. Read settings throughconfig('services.x.key').$model->update()on a model loaded withselect()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()). firstOrCreateis not atomic; back it with a unique index and catchUniqueConstraintViolationException, or usecreateOrFirst.- Octane keeps the app in memory: no request data in static properties or singletons.
APP_DEBUG=truein 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.