Come sviluppare pacchetti Laravel imperniati sui service provider: dalla pubblicazione di configurazioni, view, migrazioni e facade all’auto-discovery e al caricamento differito.
In Laravel un pacchetto è un pacchetto Composer che aggiunge funzionalità all’applicazione. Ne esistono principalmente due tipi.
Pacchetto standalone — libreria PHP generica non dipendente da Laravel (es. Carbon, Pest)
Pacchetto Laravel — pacchetto integrato con Laravel che offre rotte, controller, view, configurazione, ecc.
Questa guida tratta il secondo caso: lo sviluppo di pacchetti specifici per Laravel. Serve una conoscenza approfondita della struttura interna di Laravel — service provider, facade, pubblicazione della configurazione.
Per scrivere i test di un pacchetto usi Orchestra Testbench. Ti permette di scrivere i test del pacchetto come faresti in una normale applicazione Laravel.
Il service provider è il punto di ingresso del pacchetto. Concentra qui la registrazione delle risorse — view, config, migrazioni, rotte — in Laravel.Estende Illuminate\Support\ServiceProvider e ha due metodi: register e boot.
<?phpnamespace Acme\Courier;use Illuminate\Support\ServiceProvider;class CourierServiceProvider extends ServiceProvider{ /** * Registra i servizi del pacchetto */ public function register(): void { // qui esegui il binding nel service container $this->mergeConfigFrom( __DIR__.'/../config/courier.php', 'courier' ); $this->app->singleton(CourierManager::class, function ($app) { return new CourierManager($app['config']['courier']); }); } /** * Bootstrap dei servizi del pacchetto */ public function boot(): void { // qui registri le risorse $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'); }}
In register non registrare listener di eventi, rotte o view. Rischi di usare per errore servizi di un altro service provider non ancora caricato. Le operazioni diverse dai binding vanno sempre in boot.
mergeConfigFrom() — fondere con i valori di default
Con mergeConfigFrom() in register, i default del pacchetto si applicano anche se l’utente non ha pubblicato il file.
public function register(): void{ $this->mergeConfigFrom( __DIR__.'/../config/courier.php', 'courier' );}
mergeConfigFrom() non fonde a livelli profondi degli array annidati. In configurazioni con array multidimensionali, se l’utente definisce solo una parte, il resto delle opzioni potrebbe non essere fuso.
Passando un tag come secondo argomento di publishes(), l’utente pubblica solo le risorse desiderate.
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');}
# pubblica solo i file di configurazionephp artisan vendor:publish --tag=courier-config# pubblica tutte le risorse offerte dal providerphp artisan vendor:publish --provider="Acme\Courier\CourierServiceProvider"
Con loadViewsFrom() registri la directory delle view. Usa il namespace del secondo argomento per riferirti alle view come package::view.
public function boot(): void{ $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier');}
Dopo la registrazione, riferisci le view con il namespace del pacchetto.
Route::get('/dashboard', function () { return view('courier::dashboard');});
Laravel cerca le view in due posti: prima in resources/views/vendor/courier dell’app, poi nella directory view del pacchetto. Questo permette all’utente di personalizzarle.
Se includi componenti nel pacchetto li registri in boot.
use Illuminate\Support\Facades\Blade;use Acme\Courier\View\Components\AlertComponent;public function boot(): void{ Blade::component('courier-alert', AlertComponent::class);}
Puoi anche registrarli in blocco con un namespace di componenti.
use Illuminate\Support\Facades\Blade;public function boot(): void{ Blade::componentNamespace('Acme\\Courier\\View\\Components', 'courier');}
{{-- registrazione singola --}}<x-courier-alert />{{-- registrazione con namespace --}}<x-courier::alert />
I comandi Artisan del pacchetto si registrano con il metodo commands(). Di solito solo in ambiente 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, ]); }}
Un facade permette di richiamare i binding del service container come metodi statici.
1
Creare la classe di servizio
<?phpnamespace Acme\Courier;class CourierManager{ public function __construct( protected array $config, ) {} public function send(string $to, string $message): bool { // logica di invio messaggio return true; } public function track(string $id): array { // logica di recupero delle informazioni di tracking return ['status' => 'delivered']; }}
2
Creare la classe facade
Estendi Illuminate\Support\Facades\Facade e restituisci in getFacadeAccessor() la chiave di binding del container.
public function register(): void{ $this->app->singleton(\Acme\Courier\CourierManager::class, function ($app) { return new \Acme\Courier\CourierManager($app['config']['courier']); });}
Aggiungendo le annotazioni PHPDoc @method ai metodi del facade abiliti l’autocompletamento dell’IDE.
// invocazione del servizio tramite facadeuse Acme\Courier\Facades\Courier;Courier::send('[email protected]', 'Il tuo pacco è arrivato');$status = Courier::track('ABC-123');
DeferrableProvider — implementare il caricamento differito
I provider che eseguono solo binding nel container possono realizzare il caricamento differito implementando l’interfaccia DeferrableProvider. Il provider non viene caricato finché il servizio non è effettivamente necessario: le prestazioni dell’app migliorano.
<?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']); }); } /** * Restituisce la lista dei servizi forniti da questo provider * * @return array<int, string> */ public function provides(): array { return [CourierManager::class]; }}
Laravel compila e memorizza la lista dei servizi forniti dai deferred provider. Il provider viene caricato solo quando uno dei servizi elencati in provides() viene risolto.
Non usare DeferrableProvider in provider che devono registrare risorse (view, rotte, listener di eventi, ecc.). Con il caricamento differito, quelle risorse resterebbero non registrate.
Per testare il pacchetto in isolamento usa Orchestra Testbench. Ti permette di scrivere i test come se fossi in un’app Laravel.
composer require --dev orchestra/testbench
Nel test case fai override di getPackageProviders() per registrare il service provider del pacchetto.
<?phpnamespace Acme\Courier\Tests;use Acme\Courier\CourierServiceProvider;use Orchestra\Testbench\TestCase as BaseTestCase;class TestCase extends BaseTestCase{ /** * Registra i service provider del pacchetto */ protected function getPackageProviders($app): array { return [ CourierServiceProvider::class, ]; } /** * Registra gli alias dei facade del pacchetto */ protected function getPackageAliases($app): array { return [ 'Courier' => \Acme\Courier\Facades\Courier::class, ]; } /** * Configurazione d'ambiente per i 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]', 'messaggio di test'); $this->assertTrue($result); }}
Dipendere da illuminate/support invece che da tutto illuminate/framework ti fa dipendere solo dai componenti necessari. Mantieni piccolo l’albero delle dipendenze del pacchetto.