From laravel
Opinionated CORE Laravel 13 + Pest 4 conventions, applied by default on any Laravel project — artisan-first file creation, Eloquent/migration/queue/ scheduler patterns, fail-closed multi-tenancy, parallel-safe tests, and a "verified before done" bar. Composes with (never duplicates) maestro's stack-agnostic discipline. Use when / Trigger: editing or creating anything in a Laravel codebase — models, migrations, factories, jobs, scheduled commands, Filament resources, or Pest tests; whenever you reach for a file generator, design a schema/relationship, write a queued job or a tenant-scoped query, or are about to call a Laravel task "done."
How this skill is triggered — by the user, by Claude, or both
Slash command
/laravel:laravel-conventionsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This is the *how-we-build-here* layer for Laravel. maestro owns the
This is the how-we-build-here layer for Laravel. maestro owns the stack-agnostic discipline (prove-it, scope-discipline, native-code, finish-your-turn); this skill owns the Laravel-specific defaults those disciplines apply to. When they overlap, maestro wins on behavior and this skill supplies the idiom. Don't restate maestro here — apply it.
Anchor versions: Laravel 13, Pest 4, Filament 5, PHP 8.3+. Every code-shaped claim below is correct for those versions; if the project pins older ones, match the project, not this file.
Hand-written files drift from the framework's current stubs (namespaces, casts(), return types). Always scaffold, then edit.
php artisan make:model Invoice -mf
(add -c for a controller, --policy, -a for the full set). A model
without a factory is an exception, not the norm.php artisan make:migration create_invoices_table /
..._add_status_to_invoices_table — let the verb match the change.php artisan make:factory InvoiceFactory --model=Invoicephp artisan make:seeder, make:job, make:command, make:event,
make:listener, make:notification, make:mail, make:rule,
make:observer, make:policy, make:request, make:cast, make:enum.php artisan make:test InvoiceTest --pest (unit: --unit) — Pest, not
PHPUnit class stubs.php artisan make:filament-resource Invoice --generate,
make:filament-page, make:filament-widget, make:filament-relation-manager.vendor:publish --tag=... or a custom stub) so the next generation is right too.After generating, open the file and write in its style (native-code).
A migration that adds an FK to a not-yet-created table fails on
migrate:fresh. Migrations run in filename order, which is
timestamp, then alphabetical on ties.
make:migration timestamps to the second — two generated in the same
second tie and fall back to alphabetical filename order, which can
invert your intent. After generating related migrations, check the
ordering and rename (bump the timestamp) if parent sorts after child.$table->foreignIdFor(User::class) /
$table->foreignId('user_id')->constrained() — they encode the reference
and let you chain the delete behavior.->cascadeOnDelete() when the child is meaningless without the parent
(order line → order); ->nullOnDelete() (column must be nullable) when the
child outlives the parent (post → author); ->restrictOnDelete() when
deletion should be refused. Never leave it to the DB default.constrained() does not add an index
on some drivers — add ->index() on every FK and on any column you filter,
sort, or join by. Composite index for composite lookups; unique constraints
for real uniqueness (and as the DB-level guard behind a unique validation
rule). Indexes are cheap to add now and a migration-on-a-big-table later.state() / named states, never raw inserts.$fillable (allowlist) over $guarded. Be explicit about
mass-assignable columns.casts() method (the L11+ form, not the $casts property):
'settings' => 'array', 'published_at' => 'datetime',
'status' => StatusEnum::class, 'price' => MoneyCast::class. Money is
integer cents or a value-object cast — never a float column.belongsTo/hasMany/belongsToMany/morphTo with explicit FK args when
they're non-conventional. Return types: BelongsTo, HasMany, etc.with, load) on the read paths;
consider Model::preventLazyLoading() in non-prod so a lazy load throws
in tests instead of silently shipping.database (QUEUE_CONNECTION=database,
php artisan make:queue-table/queue:table then migrate). Easy to inspect,
fails visibly, no extra service. Sync hides ordering/idempotency bugs.routes/console.php (or the schedule definition):
->withoutOverlapping() so a slow run can't stack on the next tick, and
->onOneServer() when more than one box runs the scheduler. Pick an
explicit cadence; don't rely on default minute alignment for heavy jobs.SELECT … then UPDATE races two workers onto the same row. Use a single
conditional write — UPDATE … SET locked_by = ?, locked_at = now() WHERE id = ? AND locked_by IS NULL — and act only if it affected a row
(or lockForUpdate() inside a transaction). The winner is the writer, not
the reader.Tenant isolation that depends on every query remembering to filter will leak. Make isolation the default and the unsafe path the loud one.
BelongsToTenant trait on every tenant-owned model:
creating hook that fails closed — if there is no active tenant,
throw, never silently write a null/global tenant_id. A row with no
tenant is a leak waiting to be read by the next tenant.tenant_id. A form field or JSON key naming the tenant is
an attacker's free pick; ignore it and derive the tenant yourself.A user-facing string is a translation key, never a literal. The default locale
set is en + pt_BR; a key added to one and missing in the other is a bug
(maestro's i18n-completeness rule, in Laravel terms — don't restate it, apply it).
__() / trans() / @lang, with the key present in
lang/en AND lang/pt_BR. Filament labels use ->translateLabel(), and a
HasLabel enum resolves getLabel() through the same keys — no hardcoded
English in a resource, column, action, or notification title.lang/en/*.php missing from
lang/pt_BR/*.php (or the reverse) is the common leak — diff the pairs
(array_diff_key recursively) and treat a mismatch as a failure.->locale($notifiable->preferred_locale) (or resolve the
preferred locale on the notifiable) so the user is mailed in their language,
not the server's. Same fail-the-async-boundary reasoning as tenancy above.true). Feature test for the route/job/command; unit test for pure logic.use RefreshDatabase; for anything touching the DB — a migrated,
isolated schema per run.php artisan test --parallel
(--recreate-databases after schema changes) or pest --parallel. Parallel
uses a DB per process — drop to serial when tests share global state,
depend on ordering, race on the same fixed unique value, or hit a real
external service. Speed never buys flakiness.Model::withoutGlobalScope(TenantScope::class) or
DB::table('…')) — asserting through the scoped model can pass while the
data is leaking, a false green. Verify the barrier, not the convenience.Mail::fake(),
Notification::fake(), Bus::fake()/Queue::fake(), Event::fake(),
Http::fake(), then assert dispatched/sent. Never send real mail or hit a
real queue from a test.If a dependency is a path repository in composer.json (symlinked into
vendor/ from a sibling folder), it is source you own, not a frozen
vendor dir.
vendor/
(editing vendor/ edits the symlink target and will be lost on the next
composer install/CI).composer update <pkg> if needed,
re-run the consumer's suite). The change isn't done until the consuming app
is green against it. (This is maestro's cross-project rule, in Laravel
terms.)Don't claim a Laravel task complete on "written" or "should work" (prove-it). The done bar is three things green, this session:
php artisan migrate:fresh (or migrate:fresh --seed) succeeds — the
schema builds clean from zero, ordering and FKs included.php artisan test, parallel where safe).php artisan about / the route or page actually
renders / the command runs — no fatal on a real request.If any of the three is red, the task is in progress, and you say so in those words.
Done-bar #3 ("the page renders") is proven in a real browser, not by a compiling Blade/Livewire file (prove-it; maestro's render-verify). When the change touches a screen, run the recipe:
.env. If the
local stack isn't reachable (Redis, etc.), override on the command:
CACHE_STORE=file SESSION_DRIVER=file QUEUE_CONNECTION=sync php artisan serve
— the committed .env stays untouched, and you clean up only what you started.php artisan tinker rather than guessing or hardcoding them.storage/logs/laravel.log for that request. A page that "renders"
while logging a 500 is not verified.npx claudepluginhub mwguerra/plugins --plugin laravelBlocks Edit/Write/Bash actions until Claude investigates importers, data schemas, and user instructions. Improves output quality by forcing concrete facts before edits.