Laravel expert
Framework & language expert skills for Claude Code — idiomatic best practices for TypeScript, React, Vue, Svelte, Solid, Angular, Astro
npx -y skills add Akayashuu/agent-skills --skill laravel-expertAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when building or reviewing Laravel apps or packages — service providers, container bindings, Eloquent/N+1, form requests, queued jobs, events, config vs env, facades vs DI, or Testbench/Pest tests.
SKILL.md
8.9 KB, as published. Nobody here has run it
Laravel Expert
Overview
Idiomatic Laravel leans on the framework's conventions — the container, providers, Eloquent — instead of fighting them, while staying explicit where implicit magic becomes a footgun (mass assignment, env() after caching, N+1). These are the judgment calls that separate a clean package or app from one that leaks queries and stale config. This skill is written from the package-author perspective: most rules apply to apps too, but provider wiring, auto-discovery, publishing, and Testbench are where packages differ.
Quick Reference
| Goal | Do | Avoid |
|---|---|---|
| Read configuration at runtime | config('x.y') | env('X') outside config/*.php (returns null when config is cached) |
| Bind an interface | bind/singleton in register() | resolving other services in register() |
| Same instance everywhere | singleton | bind (new instance per resolve) when state/connection is shared |
| Register routes/migrations/publish | boot() | doing it in register() |
| Ship a package provider | extra.laravel.providers auto-discovery | telling users to edit config/app.php |
| Merge package config | mergeConfigFrom (shallow, user wins) | overwriting the published config |
| Read request input | $request->validated() from a Form Request | $request->all() into Model::create() |
| Mass-assignment safety | explicit $fillable allow-list | $guarded = [] |
| Load relations | with([...]) / withCount (eager) | lazy access in a loop (N+1) |
| Background work | ShouldQueue + idempotent handle() | assuming a job runs exactly once |
| Cross-cutting hook | event + listener | fat controller calling everything inline |
| Need a fresh/decorated instance per call | inject the contract | a facade (harder to swap/mock per-call) |
Core Patterns
Service provider: register binds, boot wires. register() may only put things into the container — never resolve another service there, it might not be registered yet. Everything that touches the booted framework (routes, migrations, publishing, views, listeners) goes in boot(). A provider can be deferred (provides() + no eager work in register()) so it only loads when one of its bindings is resolved.
public function register(): void {
$this->mergeConfigFrom(__DIR__.'/../config/package.php', 'package');
$this->app->singleton(Reporter::class, HttpReporter::class);
}
public function boot(): void {
$this->loadRoutesFrom(__DIR__.'/../routes/package.php');
$this->publishes([__DIR__.'/../config/package.php' => config_path('package.php')], 'package-config');
}
Runnable:
examples/service-provider.php
Container: bind contracts, choose singleton vs bind deliberately. bind gives a new instance every resolve; singleton gives one shared instance for the lifetime of the request/worker. Type-hinted dependencies are auto-resolved; use contextual binding (when()->needs()->give()) when one consumer needs a different implementation or a config-driven scalar.
$this->app->when(HttpReporter::class)->needs('$timeout')->giveConfig('package.timeout');
Runnable:
examples/service-provider.php
Config over env() — the cached-config gotcha. php artisan config:cache (standard in production) freezes config and unsets $_ENV for app code: any env() call outside a config/*.php file then returns null. Read env only inside config files; everywhere else read config().
Eloquent: explicit mass assignment, native casts, eager loading. Prefer an explicit $fillable allow-list over $guarded = [] (an empty guard makes every column settable from input). Push enum/date conversion into casts(). Encapsulate reusable filters in query scopes. Kill N+1 by eager-loading the relations you'll touch with with()/withCount; in dev, Model::preventLazyLoading() turns an accidental lazy load into a thrown error.
$posts = Post::published()->with(['author', 'comments'])->withCount('comments')->get();
Runnable:
examples/eager-loading.php
Form Requests: validate + authorize at the edge, read validated(). Move authorize() and rules() out of the controller; the action only runs on valid, authorized input. Reading $request->validated() returns only the rule-listed subset, which is what you should hand to create()/update() — closing the hole that $request->all() opens.
Runnable:
examples/form-request.php
Queued jobs must be idempotent. Queues retry on failure ($tries, $backoff), so a job can run more than once. Make handle() safe under re-execution — claim state atomically (transaction + lockForUpdate) rather than trusting a "did we already do it" flag. Use ShouldBeUnique to avoid duplicate dispatch, failed() to compensate, and let SerializesModels re-fetch fresh model state instead of carrying a stale snapshot.
Runnable:
examples/queued-job.php
Events/listeners decouple side effects. Emit a domain event (OrderPaid) and let listeners (email, ledger, webhook) react. Listeners can be queued (implements ShouldQueue) so slow side effects don't block the request. This keeps controllers thin and side effects independently testable.
Facades vs dependency injection. Facades are fine for framework globals in app code and read well (Cache::get). For your own collaborators — especially in package code and anything you want to swap or mock per call — inject the contract via the constructor: it's explicit, testable without Facade::swap, and works with contextual binding.
Test packages with Testbench, apps with Pest/PHPUnit. A package has no host app, so extend Orchestra Testbench's TestCase and register your provider via getPackageProviders(); it boots a minimal app so the container, config, and migrations behave realistically. App test suites instead extend the framework TestCase with RefreshDatabase.
Runnable:
examples/testbench-test.php
Common Mistakes
env()outside config files — returnsnullonceconfig:cacheruns. The bug only shows in production. Route every value through aconfig/*.phpfile.- Resolving services in
register()— order isn't guaranteed; the dependency may be unbound. Defer that work toboot()or a resolve-time closure. $guarded = []— a mass-assignment hole. Combine with$request->all()and an attacker setsis_admin. Use$fillable+validated().- N+1 from lazy loading in loops — accessing
$post->authorinsideforeachfires a query per row. Eager load up front. - Assuming a job runs once — retries, duplicate dispatch, and at-least-once delivery mean it may run twice. Design for idempotency.
singletonfor stateful-per-request things — sharing one instance across a queue worker's many jobs can leak state between jobs. Match lifetime to intent.- Editing
config/app.phpproviders for a package — useextra.laravel.providersauto-discovery instead; only the app's own providers belong there. - Overwriting published config in
boot()— usemergeConfigFrom(shallow merge) so the user's published values win.
When NOT to over-engineer
Not every class needs an interface and a binding — bind a contract when you actually have (or will have) more than one implementation or need to mock it; otherwise inject the concrete. Don't queue a job that finishes in microseconds, don't add an event for a one-listener side effect you'll never reuse, and don't reach for a custom cast or repository layer when an Eloquent model and a scope already say it clearly. Convention first; abstraction only where it earns its weight.
Sources
- Service Container · Service Providers · Facades
- Package Development — auto-discovery, publishing,
mergeConfigFrom - Configuration — the
env()/config:cachegotcha - Eloquent · Eager Loading / N+1 · Mass Assignment
- Validation / Form Requests
- Queues —
ShouldQueue, unique jobs, failed jobs · Events - Orchestra Testbench · Pest