Come implementare il caricamento differito dei servizi implementando l’interfaccia DeferrableProvider. Una funzione poco appariscente ma cruciale per ottimizzare le performance dei pacchetti.
I service provider registrati all’avvio di Laravel vengono caricati a ogni richiesta, anche se le loro funzionalità non vengono mai usate. I deferred service provider risolvono questo problema: rinviano il caricamento finché il servizio non viene effettivamente richiesto.
Questa pagina presuppone la conoscenza delle basi dello sviluppo di pacchetti. Ti consigliamo di leggere prima le nozioni di base sui service provider.
I service provider normali chiamano register() e boot() a ogni richiesta. Inizializzare ogni volta funzionalità che non vengono usate in tutte le pagine — come invio email, code o cache — è uno spreco.
// register() di questo provider viene chiamato a ogni richiestaclass ReportServiceProvider extends ServiceProvider{ public function register(): void { // ogni volta bindiamo un pesante servizio di report $this->app->singleton(ReportGenerator::class, function ($app) { return new ReportGenerator( $app->make(PdfRenderer::class), $app->make(ChartRenderer::class), $app->make(DataExporter::class), ); }); }}
Rendendolo differito, verrà inizializzato solo nelle richieste che generano realmente un report.
L’implementazione di un deferred provider si articola in tre passi.
1
Implementare DeferrableProvider
use Illuminate\Contracts\Support\DeferrableProvider;use Illuminate\Support\ServiceProvider;class ReportServiceProvider extends ServiceProvider implements DeferrableProvider{ // ...}
2
Registrare i servizi in register()
Come in un provider normale, i bind vanno in register().
public function register(): void{ $this->app->singleton(ReportGenerator::class, function ($app) { return new ReportGenerator( $app->make(PdfRenderer::class), $app->make(ChartRenderer::class), $app->make(DataExporter::class), ); }); $this->app->singleton(ReportRepository::class, function ($app) { return new ReportRepository($app->make('db')); });}
3
Restituire i servizi registrati da provides()
provides() deve restituire tutti i servizi registrati in register(). Da questa lista Laravel decide “quando richiedere quale servizio caricare questo provider”.
public function provides(): array{ return [ ReportGenerator::class, ReportRepository::class, ];}
All’avvio Laravel genera un file manifest chiamato bootstrap/cache/services.php. In esso è memorizzata la lista dei servizi forniti dai deferred provider.
// esempio (autogenerato) di bootstrap/cache/services.phpreturn [ 'providers' => [ // stessa lista di bootstrap/providers.php ], 'eager' => [ // provider a caricamento immediato App\Providers\AppServiceProvider::class, ], 'deferred' => [ // mapping nome-servizio => classe provider 'App\Services\ReportGenerator' => ReportServiceProvider::class, 'App\Repositories\ReportRepository' => ReportServiceProvider::class, 'cache' => Illuminate\Cache\CacheServiceProvider::class, 'cache.store' => Illuminate\Cache\CacheServiceProvider::class, 'queue' => Illuminate\Queue\QueueServiceProvider::class, ], 'when' => [],];
Grazie a questo manifest, Laravel sa “quale provider fornisce quale servizio” senza dover caricare i file. I provider vengono caricati solo alla prima risoluzione del servizio.
Se aggiungi o modifichi provider, rigenera il manifest.
Se in provides() manca una registrazione, quel servizio non verrà mai risolto.
public function register(): void{ $this->app->singleton(ReportGenerator::class, fn ($app) => new ReportGenerator()); // binding aggiunto $this->app->singleton('report', fn ($app) => $app->make(ReportGenerator::class));}public function provides(): array{ return [ ReportGenerator::class, 'report', // ← non dimenticare le chiavi stringa ];}
Lo stesso vale per le proprietà $bindings / $singletons: vanno anch’esse elencate in provides().
class AnalyticsServiceProvider extends ServiceProvider implements DeferrableProvider{ public $singletons = [ AnalyticsClient::class => DefaultAnalyticsClient::class, ]; public function provides(): array { // restituisci tutte le chiavi elencate in $singletons return [ AnalyticsClient::class, ]; }}
I deferred provider sono adatti quando l’unico scopo è registrare binding nel container. Non si può differire un provider che in boot() fa cose come queste.
Limitazione
Motivo
Registrazione di rotte
Le rotte vengono risolte all’avvio dell’app, quindi il deferring non funziona
Registrazione di middleware globali
La registrazione serve prima dell’elaborazione della richiesta
Registrazione di listener di eventi (sempre necessari)
Se non è registrato prima che l’evento venga emesso, non funziona
Aggiunta di direttive Blade
La registrazione serve prima della compilazione delle view
Puoi scrivere il metodo boot() in un deferred provider, ma il suo contenuto non verrà eseguito finché il servizio non viene risolto. Mettere in boot() operazioni “sempre necessarie” come rotte e middleware porta a comportamenti imprevisti.
Con il metodo when() puoi registrare il provider al verificarsi di eventi specifici. Utile per provider necessari solo in contesti specifici, come l’elaborazione dei job.
class ReportServiceProvider extends ServiceProvider implements DeferrableProvider{ public function register(): void { $this->app->singleton(ReportGenerator::class, fn () => new ReportGenerator()); } public function provides(): array { return [ReportGenerator::class]; } /** * Carica il provider anche quando l'evento indicato viene emesso, * indipendentemente dalla risoluzione dei binding. */ public function when(): array { return [ \App\Events\ReportRequested::class, ]; }}
Se l’evento restituito da when() viene emesso, il provider viene caricato anche se il servizio non è stato risolto direttamente.
namespace Acme\Analytics;use Illuminate\Contracts\Support\DeferrableProvider;use Illuminate\Support\ServiceProvider;class AnalyticsServiceProvider extends ServiceProvider implements DeferrableProvider{ public function register(): void { $this->mergeConfigFrom(__DIR__.'/../config/analytics.php', 'analytics'); $this->app->singleton(AnalyticsManager::class, function ($app) { return new AnalyticsManager($app->make('config')->get('analytics')); }); $this->app->singleton('analytics', fn ($app) => $app->make(AnalyticsManager::class)); } public function boot(): void { // usa boot() solo per la registrazione della pubblicazione if ($this->app->runningInConsole()) { $this->publishes([ __DIR__.'/../config/analytics.php' => config_path('analytics.php'), ], 'analytics-config'); } } public function provides(): array { return [ AnalyticsManager::class, 'analytics', ]; }}
mergeConfigFrom() verifica internamente se la configurazione è in cache, quindi può essere chiamato in sicurezza dentro register() di un deferred provider. Se la configurazione è già in cache, non fa nulla.
Isolare la registrazione dei comandi Artisan con runningInConsole()
La registrazione dei comandi serve solo all’avvio di Artisan, quindi usa un branch con runningInConsole(). Se il provider che fornisce i comandi è differito, ricorda di includere le classi dei comandi in provides() oppure predisponi un provider dedicato ai soli comandi.
public function boot(): void{ if ($this->app->runningInConsole()) { $this->commands([ AnalyticsFlushCommand::class, ]); }}
Molti dei provider core di Laravel sono differiti. Il design è pensato per evitare di caricare in modo immediato tutti i servizi che non vengono usati a ogni richiesta.
Provider
Servizi forniti
CacheServiceProvider
cache, cache.store, RateLimiter
QueueServiceProvider
queue, queue.worker, queue.failer
MailServiceProvider
mail.manager, mailer
RedisServiceProvider
redis, redis.connection
HashServiceProvider
hash, hash.driver
ValidationServiceProvider
validator, validation.presence
TranslationServiceProvider
translator
BroadcastServiceProvider
Broadcast
In un’applicazione fatta solo di endpoint API può capitare che MailServiceProvider o BroadcastServiceProvider non vengano mai caricati durante una richiesta.