A comprehensive guide to developing Laravel packages centered on service providers, covering everything from publishing config, views, and migrations to facades, auto-discovery, and deferred loading.
In Laravel, a package is a Composer package that adds functionality to an application. There are two broad categories:
Standalone packages — General-purpose PHP libraries with no Laravel dependency (e.g., Carbon, Pest)
Laravel packages — Packages with routes, controllers, views, config, and other features tightly integrated with Laravel
This guide covers the latter: packages built specifically for Laravel. Package development requires a deep understanding of Laravel internals, including service providers, facades, and config publishing.
When writing tests for a package, use Orchestra Testbench. It lets you write package tests just as you would tests inside a normal Laravel application.
A service provider is the entry point for a package. It is where you centralize the registration of views, config, migrations, routes, and other resources with Laravel.Service providers extend Illuminate\Support\ServiceProvider and have two methods: register and boot.
<?phpnamespace Acme\Courier;use Illuminate\Support\ServiceProvider;class CourierServiceProvider extends ServiceProvider{ /** * Register package services. */ public function register(): void { // Container bindings go here $this->mergeConfigFrom( __DIR__.'/../config/courier.php', 'courier' ); $this->app->singleton(CourierManager::class, function ($app) { return new CourierManager($app['config']['courier']); }); } /** * Bootstrap package services. */ public function boot(): void { // Resource registration goes here $this->loadRoutesFrom(__DIR__.'/../routes/web.php'); $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier'); $this->loadTranslationsFrom(__DIR__.'/../lang', 'courier'); $this->publishesMigrations([ __DIR__.'/../database/migrations' => database_path('migrations'), ]); $this->publishes([ __DIR__.'/../config/courier.php' => config_path('courier.php'), ], 'courier-config'); $this->publishes([ __DIR__.'/../resources/views' => resource_path('views/vendor/courier'), ], 'courier-views'); }}
Do not register event listeners, routes, or views inside the register method. You might accidentally use a service from another service provider that has not yet been loaded. Always perform any work beyond bindings in the boot method.
Using mergeConfigFrom() in the register method ensures that the package’s default values are used even when users have not published the config file.
public function register(): void{ $this->mergeConfigFrom( __DIR__.'/../config/courier.php', 'courier' );}
mergeConfigFrom() does not perform a deep merge for nested arrays. When a config has multi-dimensional arrays, options not defined by the user will not be merged from deeper levels.
Pass a tag as the second argument to publishes() so users can choose which resources to publish.
public function boot(): void{ $this->publishes([ __DIR__.'/../config/courier.php' => config_path('courier.php'), ], 'courier-config'); $this->publishesMigrations([ __DIR__.'/../database/migrations/' => database_path('migrations'), ], 'courier-migrations');}
# Publish only the config filephp artisan vendor:publish --tag=courier-config# Publish everything provided by the service providerphp artisan vendor:publish --provider="Acme\Courier\CourierServiceProvider"
Register a view directory with loadViewsFrom(). Use the namespace passed as the second argument to reference views with the package::view syntax.
public function boot(): void{ $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier');}
After registration, reference views using the package namespace.
Route::get('/dashboard', function () { return view('courier::dashboard');});
Laravel looks for views in two locations. It first checks resources/views/vendor/courier in the application; if nothing is found there, it falls back to the package’s view directory. This lets users customize views.
To include components in your package, register them in the boot method.
use Illuminate\Support\Facades\Blade;use Acme\Courier\View\Components\AlertComponent;public function boot(): void{ Blade::component('courier-alert', AlertComponent::class);}
You can also register components in bulk using a component namespace.
use Illuminate\Support\Facades\Blade;public function boot(): void{ Blade::componentNamespace('Acme\\Courier\\View\\Components', 'courier');}
Register Artisan commands for your package using the commands() method. It is common practice to register them only in a console environment.
use Acme\Courier\Console\Commands\InstallCommand;use Acme\Courier\Console\Commands\SyncCommand;public function boot(): void{ if ($this->app->runningInConsole()) { $this->commands([ InstallCommand::class, SyncCommand::class, ]); }}
public function register(): void{ $this->app->singleton(\Acme\Courier\CourierManager::class, function ($app) { return new \Acme\Courier\CourierManager($app['config']['courier']); });}
Adding @method PHPDoc annotations to the facade enables IDE auto-completion.
// Calling the service through the facadeuse Acme\Courier\Facades\Courier;Courier::send('[email protected]', 'Your package has arrived');$status = Courier::track('ABC-123');
Providers that only perform service container bindings can implement the DeferrableProvider interface to achieve deferred loading. The provider is not loaded until the service is actually needed, improving application performance.
<?phpnamespace Acme\Courier;use Illuminate\Contracts\Support\DeferrableProvider;use Illuminate\Support\ServiceProvider;class CourierServiceProvider extends ServiceProvider implements DeferrableProvider{ public function register(): void { $this->app->singleton(CourierManager::class, function ($app) { return new CourierManager($app['config']['courier']); }); } /** * Get the services provided by the provider. * * @return array<int, string> */ public function provides(): array { return [CourierManager::class]; }}
Laravel compiles and stores the list of services provided by deferred providers. The provider is loaded only when one of the services listed in provides() is resolved.
Do not use DeferrableProvider for providers that need to register resources (views, routes, event listeners, etc.). If the provider is deferred, those resources will never be registered.
Depending on illuminate/support rather than illuminate/framework keeps the dependency tree smaller by pulling in only the Laravel components you actually need.