illuminate/workbench
Illuminate Workbench gives Laravel developers a dedicated local workspace for building packages. Scaffold new packages, wire up service providers and autoloading, run tests, and iterate on reusable components quickly with Artisan commands before publishing them to Packagist.
illuminate/workbench is not a package you install — it is the read-only subtree split of the Illuminate\Workbench component that already ships inside laravel/framework. The 31 stars and the "[READ ONLY]" description are the tell: issues and PRs belong in laravel/framework, and the split repo exists only so Composer/libraries can resolve the component in isolation. Treat it as documentation-of-record for a tool you already have.
Minimal steps
composer show laravel/framework | grep -i version
php artisan list workbench
If workbench:* commands are missing, your framework is too old — upgrading Laravel is the fix, not composer require illuminate/workbench.php artisan workbench:create --help # see prompts/flags for your version
php artisan workbench:create # answer vendor + package name prompts
composer.json (a path repository under packages/ plus a require entry). If your version does not run it for you:
composer update
php artisan workbench:list
ls packages/<vendor>/<name>
php artisan serve
Where to look first
packages/<vendor>/<name>/src/ — your provider and classes.packages/<vendor>/<name>/composer.json — the extra.laravel.providers block is what makes auto-discovery work in the host app.composer.json — the generated repositories entry is the contract between app and package. If package code “isn’t there”, this is the first place to look.vendor/laravel/framework/src/Illuminate/Workbench) rather than this repo — it is exactly the same code, but you get the surrounding console kernel wiring.First useful use case: extract a Blade component, validation rule, or Artisan command you keep copy-pasting between projects into packages/acme/utilities, iterate against a real app (not a mock), then publish it as a standalone repo with git subtree split when it stabilises. Workbench’s whole value proposition is “develop a package with the full app available at runtime.”
The live-symlink loop (the default and the best one). Composer path repositories symlink by default, so editing packages/acme/utilities/src/Foo.php is immediately visible to the host app with zero composer update. Keep it that way; only switch to "options": { "symlink": false } if you specifically need to simulate a dist install (e.g., verifying .gitattributes export-ignore behaviour).
Provider registration and auto-discovery. Declare the provider in the package’s composer.json so the host app boots it without touching config/app.php:
{
"extra": {
"laravel": {
"providers": ["Acme\\Utilities\\UtilitiesServiceProvider"]
}
}
}
Note that this is read by the host app’s package manifest — after adding it you must composer update (or at minimum php artisan package:discover) before the provider runs.
Wire routes, views, migrations, and config in boot()/register() rather than relying on the host app. The package should be self-contained:
public function boot(): void
{
$this->loadRoutesFrom(__DIR__.'/../routes/web.php');
$this->loadViewsFrom(__DIR__.'/../resources/views', 'utilities');
$this->loadMigrationsFrom(__DIR__.'/../database/migrations');
$this->loadTranslationsFrom(__DIR__.'/../lang', 'utilities');
$this->publishes([
__DIR__.'/../config/utilities.php' => config_path('utilities.php'),
], 'utilities-config');
}
Group routes with the web middleware explicitly — Workbench does not add it for you, and ungrouped routes silently miss sessions/CSRF.
Migrations during development. php artisan migrate from the app root picks up loadMigrationsFrom paths, which is usually what you want while iterating. For ad-hoc runs outside the provider:
php artisan migrate --path=packages/acme/utilities/database/migrations
Tests live with the package. Use Orchestra Testbench so the package has its own phpunit.xml and does not depend on the host app:
cd packages/acme/utilities && composer test
This is the single most important pattern: the host app is your manual test rig, Testbench is your automated one. Without this, workbench:delete destroys your only test suite.
Git strategy — pick one deliberately.
packages/acme/utilities alongside the app; split later. Easiest, but the package inherits the app’s git history.path repository url at ../utilities instead of packages/utilities.packages/, delete when done.Multi-package workspaces. Workbench happily manages several packages under packages/ at once (workbench:list enumerates them). Keep each package’s composer.json autoload PSR-4 maps distinct (Acme\Utilities\ vs Acme\Billing\) — namespace collisions are the most common cause of “class not found” after adding a second package.
Consumer-side integration. Once the package is public, the same code works unchanged as a normal composer require acme/utilities dependency. The only thing that changes is that the path repository disappear — so verify early that nothing in the package relies on being symlinked (e.g., __DIR__ assumptions beyond the package root, or hardcoded packages/ paths in tests).
The read-only repo trap. Do not open issues or PRs at illuminate/workbench — they will be closed or ignored. Reproduce bugs in a Laravel app, then report against laravel/framework. Likewise, never add illuminate/workbench to a require block: you will either get a version-conflict with laravel/framework or a duplicate copy of the component.
Version drift between the split and your framework. illuminate/workbench is tagged in lockstep with the framework, but the split can lag on Packagist. If php artisan workbench:* behaviours differ from what you read in this repo, trust vendor/laravel/framework/src/Illuminate/Workbench — that is the code actually executing.
Path repository resolution. Two related pitfalls:
"acme/utilities": "^1.0" against a path repo with no tags fails resolution. Use "*@dev" during development.acme/utilities also exists on Packagist, Composer may resolve the published copy instead of your working tree. Force it with an explicit @dev constraint or a "canonical": true path repository entry. Symptom: your edits have no effect and composer show acme/utilities points at vendor/ instead of packages/.Autoload edits require a Composer run. Adding a class to an existing PSR-4 prefix just works (symlinked dir, Composer scans on demand). Adding a new prefix, a files autoload, or a new extra.laravel.providers entry does not — run composer update acme/utilities or composer dump-autoload. This is the number-one “why is my provider not firing” cause.
Caches lie. After changing routes, config, or a service provider, always:
php artisan optimize:clear
In dev, consider setting APP_ENV=local and never running config:cache while iterating on a package — a cached config snapshot hides your package’s config merge entirely.
workbench:delete is incomplete on purpose. It removes the directory, but historically leaves the root composer.json repositories and require entries behind. Clean those manually and run composer update, otherwise Composer will hard-fail with “could not find package.”
Windows symlinks. Composer’s path repo symlinking needs Developer Mode or an elevated shell. Without it Composer falls back to mirroring, and your “live” edits silently stop propagating. If in doubt, check whether vendor/acme/utilities is a junction.
Don’t edit vendor/acme/utilities. It should be a symlink to packages/. If it is a real directory, symlinking failed — fix that before you spend an afternoon debugging edits that go nowhere.
Package-local vendor/. Running composer install inside packages/acme/utilities creates a nested vendor/. Delete it (or gitignore it) — the host app’s autoloader already resolves the package, and a nested vendor directory causes duplicate-class and wrong-version-autoloader bugs that are genuinely nasty to diagnose.
Config publishing tags. Namespace every publishes() call with a package-specific tag (utilities-config, utilities-migrations). Generic tags collide across packages and vendor:publish --tag=config will do unpredictable things.
Extension points. Workbench itself is intentionally thin — its only real extension surface is your host app’s composer.json and the package’s extra.laravel block. If you need custom scaffolding (your org’s licence header, CI config, Pint rules), the right move is a small internal Artisan generator that calls the Workbench commands or copies a stub directory — not patching the read-only component.
When not to use it. Workbench is heavyweight: you carry a full Laravel app around. For a tiny pure-PHP helper library, a bare composer init repo plus Testbench is faster and produces a cleaner artefact. Reach for Workbench when the package genuinely needs routes, migrations, Blade, queue jobs, or anything else that wants a booted application.
How can I help you explore Laravel packages today?