Package Toolkit for Laravel v2.4.0
DocumentationDocs 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 & 1312.61.1+ · 13.12.0+
24 buildersone per resource type
MITno 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.

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()
}
}

0 lines

what you get

Three things you no longer maintain

blog::configblog::routes blog::migrationsblog::seeders blog::factoriesblog::views blog::translationsblog::assets blog::stubsblog::providers

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.

Read more
// 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 mtime

no 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.

Read more
$ 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.

Read more

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.