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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- laravel-apps Installs to ~/.agents/skills/laravel-apps/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install laravel-apps The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project laravel-apps Installs to .agents/skills/laravel-apps/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/laravel-apps.zipunzip -o laravel-apps.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) laravel-apps PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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.
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 aboutfor versions, drivers (queue, cache, session) and cached config.php artisan route:list --path=apito see existing endpoints and middleware.- Apps upgraded from Laravel 10 may still have
app/Http/Kernel.phpandapp/Console/Kernel.php; that structure keeps working. Follow what the app uses.
Skeleton orientation (Laravel 11+)
| Concern | Where |
|---|---|
| Route files, health route | bootstrap/app.php ->withRouting(web:, api:, commands:, health: '/up') |
| Global / group middleware, aliases | bootstrap/app.php ->withMiddleware(fn (Middleware $m) => ...) |
| Exception reporting and rendering | bootstrap/app.php ->withExceptions(...) |
| Scheduled tasks | routes/console.php with Schedule::command(...) |
| API routes | not 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
$fillablelists 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. Readphp artisan migrate --pretendoutput for risky changes. - Eager load with
with(), count withwithCount(), iterate large sets withchunkById()orlazyById().
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
Controllerno longer usesAuthorizesRequests, so$this->authorize()is undefined unless the app adds the trait. UseGate::authorize()or thecan: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
databasein new apps; use Redis + Horizon at higher volume.php artisan queue:work --tries=3 --max-time=3600under a process supervisor. - After each deploy run
php artisan queue:restart(orhorizon: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
ModelNotFoundExceptionunless$deleteWhenMissingModels = true. - Worker
--timeoutmust be shorter than the connection’sretry_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 inmount(). wire:modelsyncs on the next action by default; usewire:model.liveonly when you need per-keystroke updates (it costs a request each time).
Configuration
- Call
env()only inconfig/*.php. Afterphp artisan config:cachein production,env()elsewhere returnsnull. - Production deploy steps:
composer install --no-dev --optimize-autoloader,php artisan migrate --force,php artisan optimize,queue:restart. Runningmigrate --forceagainst 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.yaml
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.