Skip to content
Package Toolkit
GitHub repository

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.

PHP 8.2+ · Laravel 12 & 13 · MIT licensed

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');
}
}

src/BlogServiceProvider.php Both providers do the same thing: load, publish and tag every resource the package ships.

works with
PHP 8.2+ 8.2 · 8.3 · 8.4 · 8.5
Laravel 12 & 13 12.61.1+ · 13.12.0+
24 builders one per resource type
MIT no runtime dependency

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.

src/BlogServiceProvider.php

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'])
->hasAssetFallback(fn ($file) => route('blog.asset', $file))
->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()
}
}

0 lines

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.