Découvrez le développement de packages Laravel autour du service provider : publication de la configuration, des vues, des migrations et façades, découverte automatique et chargement différé.
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.
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.
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.
<?phpnamespace 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.
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.
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.
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 configurationphp artisan vendor:publish --tag=courier-config# Publier tous les fichiers fournis par le providerphp artisan vendor:publish --provider="Acme\Courier\CourierServiceProvider"
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.
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 />
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, ]); }}
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
<?phpnamespace 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().
public function register(): void{ $this->app->singleton(\Acme\Courier\CourierManager::class, function ($app) { return new \Acme\Courier\CourierManager($app['config']['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çadeuse 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.
<?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']); }); } /** * 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.
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.
<?phpnamespace 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'); }}
<?phpnamespace 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); }}
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.