Passer au contenu principal

Qu’est-ce qu’un package

En Laravel, un package est un package Composer qui ajoute des fonctionnalités à votre application. Il en existe deux grandes catégories :
  • Packages autonomes — bibliothèques PHP génériques indépendantes de Laravel (ex. : Carbon, Pest).
  • Packages Laravel — packages intégrés à Laravel comportant routes, contrôleurs, vues, configuration, etc.
Ce guide traite du second cas, le développement de packages spécifiques à Laravel. Il exige une compréhension approfondie de la structure interne de Laravel : service providers, façades, publication des fichiers de configuration, etc.
Pour écrire des tests de package, utilisez Orchestra Testbench. Vous pouvez alors rédiger vos tests comme dans une application Laravel classique.

Découverte automatique du package

Lors de l’installation d’un package, Laravel lit la section extra.laravel du composer.json pour enregistrer automatiquement les service providers et les façades.
"extra": {
    "laravel": {
        "providers": [
            "Acme\\Courier\\CourierServiceProvider"
        ],
        "aliases": {
            "Courier": "Acme\\Courier\\Facades\\Courier"
        }
    }
}
Avec cette configuration, l’utilisateur n’a pas besoin d’éditer manuellement bootstrap/providers.php : le package est chargé automatiquement.

Désactiver la découverte automatique

Pour désactiver la découverte automatique d’un package spécifique côté utilisateur, il suffit de le déclarer dans le composer.json de l’application.
"extra": {
    "laravel": {
        "dont-discover": [
            "acme/courier"
        ]
    }
}

Rôle du service provider

Le service provider est le point d’entrée du package. C’est là que vous enregistrez auprès de Laravel les ressources telles que vues, configuration, migrations et routes. Un service provider hérite de Illuminate\Support\ServiceProvider et possède deux méthodes : register et boot.
<?php

namespace Acme\Courier;

use Illuminate\Support\ServiceProvider;

class CourierServiceProvider extends ServiceProvider
{
    /**
     * Enregistrer les services du package
     */
    public function register(): void
    {
        // Les bindings dans le service container se font ici
        $this->mergeConfigFrom(
            __DIR__.'/../config/courier.php', 'courier'
        );

        $this->app->singleton(CourierManager::class, function ($app) {
            return new CourierManager($app['config']['courier']);
        });
    }

    /**
     * Bootstraper les services du package
     */
    public function boot(): void
    {
        // L'enregistrement des ressources se fait ici
        $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');
    }
}
N’enregistrez pas d’écouteurs d’événements, de routes ou de vues dans register. Vous risqueriez d’utiliser par erreur un service d’un autre provider pas encore chargé. Toute opération autre que les bindings doit se faire dans boot.

Publication du fichier de configuration

publishes() — exposer un fichier

En appelant publishes() dans boot, vous permettez aux utilisateurs de copier le fichier de configuration dans leur application via la commande vendor:publish.
public function boot(): void
{
    $this->publishes([
        __DIR__.'/../config/courier.php' => config_path('courier.php'),
    ]);
}
Après publication, les valeurs de configuration se récupèrent comme n’importe quelle config Laravel.
$value = config('courier.option');

mergeConfigFrom() — fusionner avec les valeurs par défaut

En utilisant mergeConfigFrom() dans register, les valeurs par défaut du package sont utilisées même lorsque l’utilisateur n’a pas publié le fichier de configuration.
public function register(): void
{
    $this->mergeConfigFrom(
        __DIR__.'/../config/courier.php', 'courier'
    );
}
mergeConfigFrom() ne fusionne pas les tableaux imbriqués en profondeur. Pour une configuration à plusieurs niveaux, si l’utilisateur n’en définit qu’une partie, les options restantes peuvent ne pas être fusionnées.

Séparer les groupes de publication par tag

En passant un tag en second argument de publishes(), l’utilisateur peut publier uniquement les ressources dont il a besoin.
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');
}
# Publier uniquement le fichier de configuration
php artisan vendor:publish --tag=courier-config

# Publier tous les fichiers fournis par le provider
php artisan vendor:publish --provider="Acme\Courier\CourierServiceProvider"

Enregistrement des routes

Chargez le fichier de routes via loadRoutesFrom(). Si le cache des routes de l’application est actif, cet appel est automatiquement ignoré.
public function boot(): void
{
    $this->loadRoutesFrom(__DIR__.'/../routes/web.php');
}
Dans ce fichier, indiquez les contrôleurs du package.
// 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');
});

Publication des migrations

publishesMigrations() permet de publier les fichiers de migration. Laravel met automatiquement à jour l’horodatage lors de la publication.
public function boot(): void
{
    $this->publishesMigrations([
        __DIR__.'/../database/migrations' => database_path('migrations'),
    ]);
}

Publication des vues

loadViewsFrom() — enregistrer les vues

loadViewsFrom() enregistre un répertoire de vues. Le second argument est le namespace qui permet de référencer les vues sous la forme package::view.
public function boot(): void
{
    $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier');
}
Une fois enregistrée, la vue est référencée via le namespace du package.
Route::get('/dashboard', function () {
    return view('courier::dashboard');
});
Laravel cherche la vue à deux endroits. Il regarde d’abord dans le dossier resources/views/vendor/courier de l’application, puis dans le répertoire des vues du package. Cela permet à l’utilisateur de personnaliser les vues.

Publier les vues

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

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

Enregistrer des composants Blade

Si votre package inclut des composants, enregistrez-les dans boot.
use Illuminate\Support\Facades\Blade;
use Acme\Courier\View\Components\AlertComponent;

public function boot(): void
{
    Blade::component('courier-alert', AlertComponent::class);
}
Vous pouvez aussi enregistrer un namespace de composants pour les déclarer en lot.
use Illuminate\Support\Facades\Blade;

public function boot(): void
{
    Blade::componentNamespace('Acme\\Courier\\View\\Components', 'courier');
}
{{-- Enregistrement individuel --}}
<x-courier-alert />

{{-- Enregistrement via namespace --}}
<x-courier::alert />

Publication des fichiers de traduction

loadTranslationsFrom() enregistre les fichiers de traduction. Les traductions se référencent sous la forme package::file.key.
public function boot(): void
{
    $this->loadTranslationsFrom(__DIR__.'/../lang', 'courier');

    $this->publishes([
        __DIR__.'/../lang' => $this->app->langPath('vendor/courier'),
    ]);
}
// Utilisation de la traduction
echo trans('courier::messages.welcome');
Pour des fichiers JSON, utilisez loadJsonTranslationsFrom().
public function boot(): void
{
    $this->loadJsonTranslationsFrom(__DIR__.'/../lang');
}

Enregistrement des commandes

Les commandes Artisan du package s’enregistrent via commands(). Il est courant de ne les enregistrer qu’en environnement console.
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,
        ]);
    }
}

Intégration à la commande optimize

Si votre package possède son propre cache, vous pouvez l’intégrer à php artisan optimize et php artisan optimize:clear via la méthode optimizes().
public function boot(): void
{
    if ($this->app->runningInConsole()) {
        $this->optimizes(
            optimize: 'courier:cache',
            clear: 'courier:clear-cache',
        );
    }
}

Ajouter des informations à la commande about

Pour ajouter des informations sur votre package à la sortie de php artisan about, utilisez AboutCommand::add().
use Illuminate\Foundation\Console\AboutCommand;

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

Créer une façade

Une façade permet d’appeler un binding du service container comme s’il s’agissait d’une méthode statique.
1

Créer la classe de service

<?php

namespace Acme\Courier;

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

    public function send(string $to, string $message): bool
    {
        // Traitement d'envoi du message
        return true;
    }

    public function track(string $id): array
    {
        // Traitement de récupération des informations de suivi
        return ['status' => 'delivered'];
    }
}
2

Créer la classe façade

Héritez de Illuminate\Support\Facades\Facade et renvoyez la clé de binding du service container depuis 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

Binder dans le service provider

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

Enregistrer dans composer.json

"extra": {
    "laravel": {
        "providers": [
            "Acme\\Courier\\CourierServiceProvider"
        ],
        "aliases": {
            "Courier": "Acme\\Courier\\Facades\\Courier"
        }
    }
}
En annotant les méthodes de la façade avec @method en PHPDoc, l’auto-complétion de l’IDE est activée.
// Appel du service via la façade
use Acme\Courier\Facades\Courier;

Courier::send('[email protected]', 'Votre colis est arrivé');
$status = Courier::track('ABC-123');

DeferrableProvider — mettre en place le chargement différé

Un provider qui n’effectue que des bindings dans le service container peut être chargé de manière différée en implémentant l’interface DeferrableProvider. Le provider n’étant chargé que lorsque le service est réellement demandé, cela améliore les performances de l’application.
<?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']);
        });
    }

    /**
     * Retourner la liste des services fournis par ce provider
     *
     * @return array<int, string>
     */
    public function provides(): array
    {
        return [CourierManager::class];
    }
}
Laravel compile et stocke la liste des services fournis par les providers différés. Le provider n’est chargé que lorsqu’un service listé dans provides() est résolu.
N’utilisez pas DeferrableProvider pour un provider qui doit enregistrer des ressources (vues, routes, écouteurs d’événements, etc.). Si le chargement est différé, ces ressources risquent de ne jamais être enregistrées.

Tester un package

Pour tester le package seul, utilisez Orchestra Testbench. Vous pouvez rédiger vos tests comme si vous étiez à l’intérieur d’une application Laravel classique.
composer require --dev orchestra/testbench
Dans le TestCase, redéfinissez getPackageProviders() pour enregistrer le service provider du package.
<?php

namespace Acme\Courier\Tests;

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

class TestCase extends BaseTestCase
{
    /**
     * Enregistrer le service provider du package
     */
    protected function getPackageProviders($app): array
    {
        return [
            CourierServiceProvider::class,
        ];
    }

    /**
     * Enregistrer les alias de façade du package
     */
    protected function getPackageAliases($app): array
    {
        return [
            'Courier' => \Acme\Courier\Facades\Courier::class,
        ];
    }

    /**
     * Configuration d'environnement de test
     */
    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]', 'Message de test');

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

Publier sur Composer

Voici les bonnes pratiques pour publier un package sur Packagist. Configuration de base du composer.json
{
    "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
}
En dépendant de illuminate/support, vous n’incluez que les composants Laravel nécessaires, pas la totalité de illuminate/framework. Cela permet de garder un arbre de dépendances léger.
Exemple de structure de répertoires
acme/courier/
├── config/
│   └── courier.php
├── database/
│   └── migrations/
│       └── 2024_01_01_000000_create_courier_logs_table.php
├── lang/
│   └── fr/
│       └── 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

Pages associées

Service providers

Détails sur les méthodes register et boot du service provider et les providers différés.

Gestion de la compatibilité de versions

Stratégie pour gérer les mises à jour majeures de Laravel et PHP et configuration de la matrice de test GitHub Actions.
Dernière modification le 13 juillet 2026