Laravel Apps skill for Codex

Laravel 12 features with policies, strict models and queues. Codex works within Laravel 12's slim skeleton: routing, middleware and exceptions in bootstrap/app.php, validation in form requests, and authorization through Gate and policies now that controllers lack authorize(). Models run in strict mode outside production, jobs are idempotent, unique and dispatched after commit, Livewire actions re-check permissions, and Pest tests cover forbidden and invalid paths.

  • Backend & APIs
  • $laravel-apps
  • 251 lines
  • 2 files
  • ~2,150 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- laravel-apps

Installs to ~/.agents/skills/laravel-apps/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $laravel-apps, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds Laravel 12 applications with the slim skeleton - bootstrap/app.php routing, middleware and exception config, Eloquent models with casts and strict mode, form requests, policies via Gate::authorize, API resources, queued jobs with retries and uniqueness, Horizon or database queues, scheduling in routes/console.php, Livewire 3 component basics, and Pest or PHPUnit feature tests. Use when adding a Laravel feature, model or endpoint, setting up queues or the scheduler, writing Livewire components, or upgrading from Laravel 10 or 11.

Use it when

  • Adding models, controllers, form requests or API resources in Laravel
  • Setting up queues, Horizon, unique jobs or the scheduler
  • Writing Livewire components that modify data
  • Upgrading a Laravel 10 or 11 app to Laravel 12

Use something else when

  • You need modern PHP language features independent of Laravel: use php-8-modern
  • The job queue design question is framework-agnostic: use background-jobs

How to check the result

php artisan test passes with cases for allowed, forbidden and invalid requests, Model::shouldBeStrict raises nothing in tests, and route:list shows the expected middleware.

The mistake it prevents

Calling env() outside config files; after config:cache in production it returns null and the feature quietly breaks.

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

--- name: laravel-apps description: Builds Laravel 12 applications with the slim skeleton - bootstrap/app.php routing, middleware and exception config, Eloquent models with casts and… license: MIT ---

Done well means: input is validated in form requests, authorization goes through policies, Eloquent never lazy-loads silently, slow work is queued after the transaction commits, configuration reads env() only inside config/*.php, and feature tests cover each route’s allowed, forbidden and invalid cases.

Inspect first

  • Read AGENTS.md, composer.json (Laravel version, Livewire, Sanctum, Horizon, Pest), bootstrap/app.php (routing, middleware, exceptions), and .env.example.
  • php artisan about for versions, drivers (queue, cache, session) and cached config.
  • php artisan route:list --path=api to see existing endpoints and middleware.
  • Apps upgraded from Laravel 10 may still have app/Http/Kernel.php and app/Console/Kernel.php; that structure keeps working. Follow what the app uses.

Skeleton orientation (Laravel 11+)

ConcernWhere
Route files, health routebootstrap/app.php ->withRouting(web:, api:, commands:, health: '/up')
Global / group middleware, aliasesbootstrap/app.php ->withMiddleware(fn (Middleware $m) => ...)
Exception reporting and renderingbootstrap/app.php ->withExceptions(...)
Scheduled tasksroutes/console.php with Schedule::command(...)
API routesnot present by default; php artisan install:api adds routes/api.php and Sanctum

Make the pieces

php artisan make:model Order -mfs --policy     # model, migration, factory, seeder, policy
php artisan make:controller OrderController --api --model=Order
php artisan make:request StoreOrderRequest
php artisan make:resource OrderResource
php artisan make:job SyncOrderToErp

Models

class Order extends Model
{
    use HasFactory;

    protected $fillable = ['shipping_address', 'notes'];   // never user_id, status, totals

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

    public function customer(): BelongsTo { return $this->belongsTo(User::class, 'user_id'); }
    public function items(): HasMany { return $this->hasMany(OrderItem::class); }
}
// AppServiceProvider::boot()
Model::shouldBeStrict(! $this->app->isProduction());   // lazy loading, silently discarded attributes, missing attributes all throw
  • $fillable lists only fields users may set. Assign ownership and state explicitly: $request->user()->orders()->create($request->validated()).
  • Migrations: foreignId('user_id')->constrained()->cascadeOnDelete(), unique() and indexes for every lookup column. Read php artisan migrate --pretend output for risky changes.
  • Eager load with with(), count with withCount(), iterate large sets with chunkById() or lazyById().

Controllers, validation, authorization

class StoreOrderRequest extends FormRequest
{
    public function authorize(): bool { return true; }   // policy check happens in the controller
    public function rules(): array
    {
        return [
            'shipping_address' => ['required', 'string', 'max:500'],
            'items' => ['required', 'array', 'min:1', 'max:50'],
            'items.*.sku' => ['required', 'string', 'exists:products,sku'],
            'items.*.qty' => ['required', 'integer', 'between:1,100'],
        ];
    }
}

class OrderController extends Controller
{
    public function index(Request $request)
    {
        return OrderResource::collection(
            $request->user()->orders()->with('items')->latest()->cursorPaginate(50)
        );
    }

    public function store(StoreOrderRequest $request, PlaceOrder $placeOrder)
    {
        Gate::authorize('create', Order::class);
        $order = $placeOrder($request->user(), $request->validated());
        return (new OrderResource($order->load('items')))->response()->setStatusCode(201);
    }

    public function show(Order $order)
    {
        Gate::authorize('view', $order);
        return new OrderResource($order->load('items'));
    }
}
  • The Laravel 11+ base Controller no longer uses AuthorizesRequests, so $this->authorize() is undefined unless the app adds the trait. Use Gate::authorize() or the can: middleware.
  • Route model binding (Order $order) does not check ownership. Either authorize with a policy, or scope bindings (->scopeBindings() for nested routes).
  • Put multi-step writes in an action/service class inside DB::transaction().
  • API Resources control the response shape; never return models directly from APIs (hidden attributes help, but resources make the contract explicit).

Queues

class SyncOrderToErp implements ShouldQueue, ShouldBeUnique
{
    use Queueable;

    public int $tries = 5;
    public int $timeout = 30;

    public function __construct(public Order $order) {}
    public function uniqueId(): string { return (string) $this->order->id; }
    public function backoff(): array { return [10, 60, 300]; }

    public function handle(ErpClient $erp): void
    {
        $erp->upsertOrder($this->order);    // idempotent: safe to run twice
    }

    public function failed(Throwable $e): void
    {
        report($e);
    }
}

SyncOrderToErp::dispatch($order)->afterCommit();
  • Default queue connection is database in new apps; use Redis + Horizon at higher volume. php artisan queue:work --tries=3 --max-time=3600 under a process supervisor.
  • After each deploy run php artisan queue:restart (or horizon:terminate) or workers keep old code in memory.
  • Models passed to jobs are re-fetched by id when the job runs; a deleted model throws ModelNotFoundException unless $deleteWhenMissingModels = true.
  • Worker --timeout must be shorter than the connection’s retry_after, or jobs run twice.

Scheduling

// routes/console.php
Schedule::command('orders:expire-stale')->hourly()->withoutOverlapping()->onOneServer();
Schedule::job(new PruneOldExports)->dailyAt('03:00');

Cron runs php artisan schedule:run every minute. onOneServer() needs a shared cache (Redis, database) across servers.

Livewire basics

class OrderNotes extends Component
{
    #[Locked] public int $orderId;
    #[Validate('required|string|max:2000')] public string $notes = '';

    public function save(): void
    {
        $order = Order::findOrFail($this->orderId);
        Gate::authorize('update', $order);
        $this->validate();
        $order->update(['notes' => $this->notes]);
        $this->dispatch('notes-saved');
    }

    public function render() { return view('livewire.order-notes'); }
}
  • Public properties round-trip through the browser and can be tampered with. Mark ids #[Locked] and authorize inside every action, not only in mount().
  • wire:model syncs on the next action by default; use wire:model.live only when you need per-keystroke updates (it costs a request each time).

Configuration

  • Call env() only in config/*.php. After php artisan config:cache in production, env() elsewhere returns null.
  • Production deploy steps: composer install --no-dev --optimize-autoloader, php artisan migrate --force, php artisan optimize, queue:restart. Running migrate --force against production needs the user’s explicit go-ahead.

Testing (Pest shown)

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

it('queues the ERP sync after placing an order', function () {
    Queue::fake();
    $this->actingAs($user = User::factory()->create())
        ->postJson('/api/orders', validOrderPayload())
        ->assertCreated();
    Queue::assertPushed(SyncOrderToErp::class);
});

Use RefreshDatabase (or LazilyRefreshDatabase), Http::fake() for outbound calls, Mail::fake(), Queue::fake(), and Livewire::test(OrderNotes::class) for components.

Verification

php artisan test            # or vendor/bin/pest
vendor/bin/pint --test      # if Pint is configured
vendor/bin/phpstan analyse  # if Larastan is configured
php artisan route:list --path=orders

Output template

## Laravel change: <summary>
- Routes: <method uri -> controller@action, middleware>
- Models/migrations: <tables, casts, constraints>
- Validation/authorization: <form requests, policy abilities>
- Jobs/schedule: <job, tries, backoff, uniqueness>
- Tests: <cases>

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Laravel Apps"
  short_description: "Laravel 12 features with policies, strict models and queues"
  default_prompt: "$laravel-apps Add an orders API to our Laravel 12 app with validation, policies and tests"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc