for laravel package authors
Describe what your package has. The toolkit wires it.
Config, routes, migrations, views, assets, commands — declared once in
configure(). The provider does the loadX() calls, the
publishes() mapping and the publish tags for you, at the moment Laravel
expects each of them.
54 lines
class BlogServiceProvider extends ServiceProvider{ public function register(): void { $this->mergeConfigFrom(__DIR__ . '/../config/blog.php', 'blog'); } public function boot(): void { $this->loadRoutesFrom(__DIR__ . '/../routes/web.php'); $this->loadRoutesFrom(__DIR__ . '/../routes/api.php'); $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); $this->loadViewsFrom(__DIR__ . '/../resources/views', 'blog'); $this->loadTranslationsFrom(__DIR__ . '/../lang', 'blog'); $this->loadJsonTranslationsFrom(__DIR__ . '/../lang'); Blade::componentNamespace('Vendor\\Blog\\View\\Components', 'blog'); require __DIR__ . '/../routes/channels.php'; if (! $this->app->runningInConsole()) { return; } $this->commands([ Commands\PruneDraftsCommand::class, Commands\ReindexCommand::class, ]); $this->publishes([ __DIR__ . '/../config/blog.php' => config_path('blog.php'), ], 'blog-config'); $this->publishes([ __DIR__ . '/../database/migrations' => database_path('migrations'), ], 'blog-migrations'); $this->publishes([ __DIR__ . '/../database/seeders/BlogSeeder.php' => database_path('seeders/BlogSeeder.php'), ], 'blog-seeders'); $this->publishes([ __DIR__ . '/../resources/views' => resource_path('views/vendor/blog'), ], 'blog-views'); $this->publishes([ __DIR__ . '/../lang' => lang_path('vendor/blog'), ], 'blog-translations'); $this->publishes([ __DIR__ . '/../resources/dist' => public_path('vendor/blog'), ], 'blog-assets'); }}
class BlogServiceProvider extends PackageServiceProvider{ public function configure(Packager $packager): void { $packager ->name('Blog') ->hasConfig() ->hasRoutes(['web.php', 'api.php']) ->hasBroadcastChannels(['channels.php']) ->hasMigrations() ->hasSeeders() ->hasTranslations() ->hasViews() ->hasComponentNamespace('Vendor\\Blog\\View\\Components') ->hasCommands() ->hasAssets() ->hasQuickInstall(); }}
src/BlogServiceProvider.php Both providers do the same thing: load, publish and tag every resource the package ships.
works with
the vocabulary
Everything a package ships, declared
One builder per resource type, each with a matching bootX() or
publishX() on the provider. Switch them on and off — this is the provider
you would write.
class BlogServiceProvider extends PackageServiceProvider{ public function configure(Packager $packager): void { $packager ->name('Blog') ->hasConfig() ->hasRoutes(['web.php', 'api.php']) ->hasBroadcastChannels(['channels.php']) ->hasMigrations() ->hasSeeders() ->hasFactories() ->hasTranslations() ->hasViews() ->hasComponents(['alert' => Alert::class]) ->hasComponentNamespace('Vendor\\Blog\\View\\Components') ->hasViewComposer('blog::sidebar', SidebarComposer::class) ->hasSharedDataForAllViews(['brand' => 'Blog']) ->hasAssets() ->hasViteAssets(['resources/js/blog.js']) ->hasMiddlewareAliases(['author' => EnsureAuthor::class]) ->hasMiddlewareGroups(['web' => [TrackReads::class]]) ->hasMiddlewareGlobals([TrackReads::class]) ->hasEvents([Published::class => Notify::class]) ->hasSubscribers([BlogSubscriber::class]) ->hasCommands() ->hasOptimizeCommands('blog:cache', 'blog:clear') ->hasStubs() ->hasProviders(['../stubs/BlogProvider.stub']) ->hasInstallCommand() ->hasAbout() }}
what you get
Three things you no longer maintain
predictable tags
Publishing, solved
Every resource lands under package::group, in the directory Laravel
expects to find it in. The classic flat package-group format is one call away,
and both can be registered at once.
// resources/views/layout.blade.php<script src="{{ app(PublishedAssets::class) ->url('blog', $js) }}"></script> → /vendor/blog/app.js?id=1786230412 copied on first resolve, cache-busted by mtimeno publish step
Assets that stay published
The asset mirror keeps public/vendor/your-package in step with what you
ship — lazily, atomically, and cache-busted by the published copy's mtime, so an upgrade
takes effect without anyone running a command.
$ php artisan blog:install 🚀 Installing Blog (1/3) Publishing configuration... ✅ Published config(2/3) Publishing migrations... ✅ Published migrations(3/3) Publishing assets... ✅ Published assets ✨ Blog installed successfully!one call, one command
An installer your users trust
hasQuickInstall() registers php artisan blog:install, publishes
what the package declared and reports what it did — with presets, hooks and
environment-aware publishing when you want them.
the mental model
Two objects, and nothing hidden
There are exactly two objects to keep in your head, and nothing is hidden behind either of them:
every hasX() on the description has a matching bootX() or publishX() on the provider, and you
can call, override or skip any of them.
what the package has
Packager
The description. Every method is a hasX() builder returning
$this, so configuration is one chain — and it validates eagerly, so a missing
directory fails while you are building the package, not on a user's machine six months
later.
$packager ->name('Blog') ->hasViews();
when Laravel is ready for it
PackageServiceProvider
The machinery. It creates the Packager, hands it to your
configure(), then acts on the description at the two moments Laravel gives it:
register() and boot().
$this->loadViewsFrom($views, 'blog'); $this->publishes([ $views => resource_path('views/vendor/blog'),], 'blog::views');
Start with a working package
The quickstart goes from an empty directory to a package with config, routes, views,
migrations and its own artisan install command — in one page.