Skip to main content

What is a package?

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.

Package auto-discovery

When a package is installed, Laravel reads the extra.laravel section of composer.json and automatically registers service providers and facades.
"extra": {
    "laravel": {
        "providers": [
            "Acme\\Courier\\CourierServiceProvider"
        ],
        "aliases": {
            "Courier": "Acme\\Courier\\Facades\\Courier"
        }
    }
}
With this configuration in place, users do not need to manually edit bootstrap/providers.php — the package is loaded automatically.

Disabling auto-discovery

Users who want to disable auto-discovery for a specific package can do so in their application’s composer.json.
"extra": {
    "laravel": {
        "dont-discover": [
            "acme/courier"
        ]
    }
}

The role of service providers

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.
<?php

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

Publishing config files

publishes() — publish files

Calling publishes() in the boot method allows users to copy config files into their application with the vendor:publish command.
public function boot(): void
{
    $this->publishes([
        __DIR__.'/../config/courier.php' => config_path('courier.php'),
    ]);
}
Once published, config values are accessible the usual way.
$value = config('courier.option');

mergeConfigFrom() — merge with defaults

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.

Organizing with publish tags

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 file
php artisan vendor:publish --tag=courier-config

# Publish everything provided by the service provider
php artisan vendor:publish --provider="Acme\Courier\CourierServiceProvider"

Registering routes

Use loadRoutesFrom() to load a routes file. It is automatically skipped when the application’s route cache is active.
public function boot(): void
{
    $this->loadRoutesFrom(__DIR__.'/../routes/web.php');
}
Reference your package’s controllers inside the routes file.
// routes/web.php
use Acme\Courier\Http\Controllers\TrackingController;
use Illuminate\Support\Facades\Route;

Route::prefix('courier')->group(function () {
    Route::get('/track/{id}', [TrackingController::class, 'show'])
        ->name('courier.track');
});

Publishing migrations

Use publishesMigrations() to publish migration files. Laravel automatically updates the timestamps when the files are published.
public function boot(): void
{
    $this->publishesMigrations([
        __DIR__.'/../database/migrations' => database_path('migrations'),
    ]);
}

Publishing views

loadViewsFrom() — register views

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.

Publishing views

public function boot(): void
{
    $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier');

    $this->publishes([
        __DIR__.'/../resources/views' => resource_path('views/vendor/courier'),
    ], 'courier-views');
}

Registering Blade components

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');
}
{{-- Individual registration --}}
<x-courier-alert />

{{-- Namespace registration --}}
<x-courier::alert />

Publishing translation files

Register translation files with loadTranslationsFrom(). Reference translations using the package::file.key syntax.
public function boot(): void
{
    $this->loadTranslationsFrom(__DIR__.'/../lang', 'courier');

    $this->publishes([
        __DIR__.'/../lang' => $this->app->langPath('vendor/courier'),
    ]);
}
// Using translations
echo trans('courier::messages.welcome');
For JSON translation files, use loadJsonTranslationsFrom().
public function boot(): void
{
    $this->loadJsonTranslationsFrom(__DIR__.'/../lang');
}

Registering commands

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,
        ]);
    }
}

Integrating with the optimize command

If your package has its own cache, use the optimizes() method to integrate with php artisan optimize and php artisan optimize:clear.
public function boot(): void
{
    if ($this->app->runningInConsole()) {
        $this->optimizes(
            optimize: 'courier:cache',
            clear: 'courier:clear-cache',
        );
    }
}

Adding information to the about command

Use AboutCommand::add() to include package information in the output of php artisan about.
use Illuminate\Foundation\Console\AboutCommand;

public function boot(): void
{
    AboutCommand::add('Courier Package', fn () => ['Version' => '1.0.0']);
}

Creating a facade

A facade lets you call service container bindings as if they were static methods.
1

Create the service class

<?php

namespace Acme\Courier;

class CourierManager
{
    public function __construct(
        protected array $config,
    ) {}

    public function send(string $to, string $message): bool
    {
        // Message sending logic
        return true;
    }

    public function track(string $id): array
    {
        // Tracking information retrieval logic
        return ['status' => 'delivered'];
    }
}
2

Create the facade class

Extend Illuminate\Support\Facades\Facade and return the service container binding key from getFacadeAccessor().
<?php

namespace Acme\Courier\Facades;

use Illuminate\Support\Facades\Facade;

/**
 * @method static bool send(string $to, string $message)
 * @method static array track(string $id)
 *
 * @see \Acme\Courier\CourierManager
 */
class Courier extends Facade
{
    protected static function getFacadeAccessor(): string
    {
        return \Acme\Courier\CourierManager::class;
    }
}
3

Bind it in the service provider

public function register(): void
{
    $this->app->singleton(\Acme\Courier\CourierManager::class, function ($app) {
        return new \Acme\Courier\CourierManager($app['config']['courier']);
    });
}
4

Register it in composer.json

"extra": {
    "laravel": {
        "providers": [
            "Acme\\Courier\\CourierServiceProvider"
        ],
        "aliases": {
            "Courier": "Acme\\Courier\\Facades\\Courier"
        }
    }
}
Adding @method PHPDoc annotations to the facade enables IDE auto-completion.
// Calling the service through the facade
use Acme\Courier\Facades\Courier;

Courier::send('[email protected]', 'Your package has arrived');
$status = Courier::track('ABC-123');

DeferrableProvider — implementing deferred loading

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.
<?php

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

Testing a package

Use Orchestra Testbench to test your package in isolation. It lets you write tests as if you were inside a normal Laravel application.
composer require --dev orchestra/testbench
Override getPackageProviders() in your test case to register the package’s service provider.
<?php

namespace Acme\Courier\Tests;

use Acme\Courier\CourierServiceProvider;
use Orchestra\Testbench\TestCase as BaseTestCase;

class TestCase extends BaseTestCase
{
    /**
     * Register the package's service providers.
     */
    protected function getPackageProviders($app): array
    {
        return [
            CourierServiceProvider::class,
        ];
    }

    /**
     * Register the package's facade aliases.
     */
    protected function getPackageAliases($app): array
    {
        return [
            'Courier' => \Acme\Courier\Facades\Courier::class,
        ];
    }

    /**
     * Define the test environment configuration.
     */
    protected function defineEnvironment($app): void
    {
        $app['config']->set('courier.api_key', 'test-key');
    }
}
<?php

namespace Acme\Courier\Tests\Feature;

use Acme\Courier\Facades\Courier;
use Acme\Courier\Tests\TestCase;

class CourierTest extends TestCase
{
    public function test_can_send_message(): void
    {
        $result = Courier::send('[email protected]', 'Test message');

        $this->assertTrue($result);
    }
}

Publishing to Composer

Best practices for publishing your package to Packagist. Basic composer.json configuration
{
    "name": "acme/courier",
    "description": "A Laravel courier package",
    "type": "library",
    "license": "MIT",
    "require": {
        "php": "^8.2",
        "illuminate/support": "^11.0||^12.0||^13.0"
    },
    "require-dev": {
        "orchestra/testbench": "^9.0||^10.0",
        "phpunit/phpunit": "^11.0"
    },
    "autoload": {
        "psr-4": {
            "Acme\\Courier\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Acme\\Courier\\Tests\\": "tests/"
        }
    },
    "extra": {
        "laravel": {
            "providers": [
                "Acme\\Courier\\CourierServiceProvider"
            ],
            "aliases": {
                "Courier": "Acme\\Courier\\Facades\\Courier"
            }
        }
    },
    "minimum-stability": "stable",
    "prefer-stable": true
}
Depending on illuminate/support rather than illuminate/framework keeps the dependency tree smaller by pulling in only the Laravel components you actually need.
Example directory structure
acme/courier/
├── config/
│   └── courier.php
├── database/
│   └── migrations/
│       └── 2024_01_01_000000_create_courier_logs_table.php
├── lang/
│   └── en/
│       └── messages.php
├── resources/
│   └── views/
│       └── dashboard.blade.php
├── routes/
│   └── web.php
├── src/
│   ├── Console/
│   │   └── Commands/
│   │       └── InstallCommand.php
│   ├── Facades/
│   │   └── Courier.php
│   ├── Http/
│   │   └── Controllers/
│   │       └── TrackingController.php
│   ├── CourierManager.php
│   └── CourierServiceProvider.php
├── tests/
│   ├── Feature/
│   └── TestCase.php
├── composer.json
└── README.md

Service providers

Learn about the register and boot methods of service providers and the details of deferred providers.

Package version compatibility

Learn how to manage Laravel and PHP version compatibility for your packages, including GitHub Actions test matrix configuration.
Last modified on April 14, 2026