juststeveking/laravel-stubs
Laravel Stubs provides publishable stub files for Laravel's make commands, letting you customize generated classes, controllers, models, and other scaffolding to match your project's conventions without editing vendor files.
The mental model to hold onto: this package is a delivery mechanism for files, not a runtime feature. Laravel's make:* commands already check base_path('stubs/{name}.stub') before falling back to the framework's bundled template. This package ships an opinionated set of those .stub files. Publish them and you are done — the framework does the rest.
Install as a dev dependency.
composer require --dev juststeveking/laravel-stubs
Publish the stubs into the project root. Use the package's publish command (or Laravel's built-in php artisan stub:publish if the package relies on it) so files land in stubs/ at the project root:
php artisan vendor:publish --tag=stubs
# or
php artisan stub:publish
Verify what actually got published. Do not assume. List the directory and compare against the stub names Laravel's make commands expect for your Laravel major:
ls stubs/
First use case — read before you generate. Generate one representative artifact and read it end to end:
php artisan make:model Invoice -mf
php artisan make:controller InvoiceController --resource
php artisan make:request StoreInvoiceRequest
php artisan make:test InvoiceTest
Open the generated files. You are looking for declare(strict_types=1), final on classes, typed properties, promoted constructors, and — critically — that {{ namespace }}, {{ class }}, {{ rootNamespace }} were all substituted, not left as literal text.
Decide now: keep the dependency or vendor the files. Once published, the composer package does nothing for make:* output. Everything downstream is driven by stubs/ in your repo. Most teams should commit stubs/, drop the requirement, and treat the templates as first-class source.
Partial override. You do not need to override every stub. Delete the ones you don't care about; a missing file means the framework default is used. A minimal, high-value set is typically: model.stub, controller.stub, controller.plain.stub, request.stub, policy.stub, job.stub, test.stub, migration.create.stub, migration.stub. Fewer files means less to audit on every Laravel upgrade.
Edit literals, preserve tokens. The correct change is always to surrounding text — never to the placeholder names. Adding declare(strict_types=1);, changing class {{ class }} to final class {{ class }}, adding a promoted constructor — all safe. Renaming {{ class }} to {{ modelClass }} silently breaks generation (see Gotchas).
Namespace-aware stubs for non-App\ code. In composer.json you can register additional namespaces for domain code; the stubs' {{ namespace }} token resolves per-command, so the same model.stub works for both App\Models\ and Domain\Billing\Models\. Do not hardcode App\ anywhere — check each published stub for it and replace with {{ rootNamespace }} or {{ namespace }}.
Golden-file tests. The single most valuable habit with stubs: a small test suite that generates artifacts into a temp directory and asserts on the output. Pest/PHPUnit both work:
it('generates a compliant model', function () {
$this->artisan('make:model', ['name' => 'FakeInvoice', '--factory' => true]);
$contents = File::get(app_path('Models/FakeInvoice.php'));
expect($contents)
->toContain('declare(strict_types=1);')
->toContain('final class FakeInvoice')
->not->toContain('{{');
File::delete(app_path('Models/FakeInvoice.php'));
});
This converts the silent-failure mode (see Gotchas) into a loud one. Run it in CI on every PR that touches stubs/.
Lint-and-analyse the output, not just the stubs. Add a CI step that generates one artifact per stub type and runs Pint + PHPStan/Larastan over the results. Generated code that fails your own ruleset is a stub bug, and this catches it at PR time rather than weeks later in review.
Pair with your formatter. Run vendor/bin/pint --dirty after generation during local development so hand-edited stubs quickly reveal formatting drift. If Pint always rewrites your stub output, the stub is fighting the ruleset — fix the stub, not the formatter.
Local make command wrapping. If you have a house php artisan make:domain-model, have it read base_path('stubs/model.stub') directly rather than bundling its own templates. One source of truth for scaffolding.
Review the stub diff like a design doc. A PR that changes stubs/model.stub affects every model generated from that day forward. It deserves a reviewer from whoever owns the code standard, and it deserves a note in CONTRIBUTING.md about why the stub differs from stock Laravel.
The failure mode is silent. If stubs/model.stub is misspelled, or the filename doesn't match what your Laravel version's make:model looks for, the framework default is used with no error, no warning, no log line. Your generated code just quietly stops matching the standard. After publishing, and again after every Laravel major upgrade, generate one artifact per stub type and grep for declare(strict_types=1) (or whatever your marker is). This is a ten-minute recurring job that prevents a year of drift.
Stub filenames change between Laravel majors. test.stub vs. pest-flavoured stubs, controller.plain.stub, migration.create.stub vs. migration.stub — the expected names track the framework skeleton, not the package. Add "re-verify vendored stubs against the new skeleton" to your upgrade checklist. Do not assume a package release exists for the new major; check the tags.
--force on publish overwrites your edits. Commit stubs/ before re-publishing anything, and read the diff. Equally, publishing without --force leaves existing files untouched — so a half-populated stubs/ directory can look complete while half the stubs are still framework defaults.
Not every command supplies every token. {{ factory }}, {{ migration }}, {{ modelVariable }}, {{ factoryNamespace }} are injected by specific make commands. If you use a token in a stub whose command doesn't provide it, you get the literal {{ token }} in the generated file. Generate once per stub after any edit and grep for {{.
final and readonly have downstream costs. final class blocks Mockery partial mocks; readonly promoted properties conflict with Eloquent's mass-assignment and hydration. Apply final where it's cheap (controllers, requests, policies, jobs) and be deliberate in the model stub — especially if your test suite mocks models.
declare(strict_types=1) in generated code can surface latent type-juggling. Usually a win, occasionally a surprise in a controller that passed "5" where an int was expected. Cheap to fix when generated, expensive to fix in twenty hand-written files — another argument for greenfield-only adoption.
Keep stubs/ out of autoload and static analysis. It contains .stub extensions so Composer won't autoload it, but check phpstan.neon excludePaths and any pint.json exclude if the files ever get renamed to .php by tooling.
Second stub-publishing package in the same repo is a trap. Stub resolution order and publish conflicts produce "why did my model generate wrong" bugs that are genuinely slow to diagnose. One owner for stubs/, one package (ideally zero, once vendored).
Extension points are the files themselves. There is no config file to tune, no service provider behaviour to override that matters day-to-day. The extension point is: open stubs/foo.stub, edit it, generate, diff. If you find yourself wanting a config knob, you want a custom make command instead.
Check the licence before vendoring. Copying the stub files into your repo is the recommended end state; make sure the licence permits it, and keep an attribution note in stubs/README.md along with a line explaining how your stubs differ from stock Laravel — otherwise a new hire will copy a tutorial pattern and undo the standard in one commit.
How can I help you explore Laravel packages today?