Explica cómo cargar servicios de forma diferida implementando la interfaz DeferrableProvider. Una funcionalidad discreta pero clave para optimizar el rendimiento de un paquete.
Los service providers registrados durante el arranque de Laravel se cargan siempre, incluso si sus funcionalidades no se usan ni una vez durante la petición. Los service providers diferidos (Deferred Service Provider) resuelven este problema: retrasan la carga del provider hasta que sus servicios se usan realmente.
Esta página da por sabido el contenido de Fundamentos del desarrollo de paquetes. Es recomendable entender primero cómo funciona un service provider normal.
Un service provider normal ejecuta register() y boot() en cada petición. Es un desperdicio inicializar en todas las páginas funcionalidades como el envío de correos, colas o caché si no se usan en muchas de ellas.
// register() de este provider se llama en todas las peticionesclass ReportServiceProvider extends ServiceProvider{ public function register(): void { // Enlazamos un servicio de reports pesado en cada request $this->app->singleton(ReportGenerator::class, function ($app) { return new ReportGenerator( $app->make(PdfRenderer::class), $app->make(ChartRenderer::class), $app->make(DataExporter::class), ); }); }}
Si haces este provider diferido, solo se inicializará en las peticiones que generen realmente un report.
La implementación de un provider diferido consta de tres pasos.
1
Implementar DeferrableProvider
use Illuminate\Contracts\Support\DeferrableProvider;use Illuminate\Support\ServiceProvider;class ReportServiceProvider extends ServiceProvider implements DeferrableProvider{ // ...}
2
Enlazar los servicios en register()
Igual que en un provider normal, define los bindings en 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
Devolver los servicios registrados en provides()
provides() debe devolver todos los servicios que has enlazado en register(). Laravel usa esta lista para saber al pedir qué servicio debe cargar este provider.
public function provides(): array{ return [ ReportGenerator::class, ReportRepository::class, ];}
En el arranque, Laravel genera un archivo llamado bootstrap/cache/services.php. Este manifest contiene la lista de servicios que ofrecen los providers diferidos.
// Ejemplo de bootstrap/cache/services.php (generado automáticamente)return [ 'providers' => [ // La misma lista de bootstrap/providers.php ], 'eager' => [ // Providers de carga inmediata App\Providers\AppServiceProvider::class, ], 'deferred' => [ // Mapping servicio => clase del 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' => [],];
Gracias a este manifest, Laravel sabe qué provider ofrece cada servicio sin necesidad de cargar el archivo del provider. El provider real solo se carga cuando el servicio se resuelve por primera vez.
Cuando añadas o modifiques providers, regenera el manifest.
php artisan optimize:clear# o bienphp artisan clear-compiled
Si en provides()te olvidas de algún servicio, ese servicio no se resolverá nunca.
public function register(): void{ $this->app->singleton(ReportGenerator::class, fn ($app) => new ReportGenerator()); // Binding adicional $this->app->singleton('report', fn ($app) => $app->make(ReportGenerator::class));}public function provides(): array{ return [ ReportGenerator::class, 'report', // ← no olvides incluir también las claves por string ];}
Lo mismo aplica si usas las propiedades $bindings / $singletons.
class AnalyticsServiceProvider extends ServiceProvider implements DeferrableProvider{ public $singletons = [ AnalyticsClient::class => DefaultAnalyticsClient::class, ]; public function provides(): array { // Devuelve todas las claves declaradas en $singletons return [ AnalyticsClient::class, ]; }}
Los providers diferidos son adecuados solo para providers cuyo único cometido es registrar bindings en el contenedor. Los providers que hacen lo siguiente en boot() no pueden ser diferidos.
Limitación
Motivo
Registro de rutas
Las rutas se resuelven al arrancar la aplicación, así que en modo diferido no funcionaría.
Registro de middlewares globales
Debe estar registrado antes de procesar la petición.
Registro de listeners de eventos (los que se necesitan siempre)
Si el listener no está registrado antes de que se dispare el evento, no se ejecuta.
Añadir directivas de Blade
Debe estar registrado antes de compilar las vistas.
Puedes escribir un método boot() en un provider diferido, pero su contenido no se ejecutará hasta que se resuelva el servicio. Si en boot() haces cosas «que siempre se necesitan», como rutas o middlewares, obtendrás un comportamiento inesperado.
Con el método when() puedes hacer que un provider se registre cuando se dispare un evento concreto. Es útil para providers que solo se necesitan en contextos específicos, como procesos de jobs.
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]; } /** * Además de al resolver los bindings, * carga el provider cuando se disparen los eventos indicados. */ public function when(): array { return [ \App\Events\ReportRequested::class, ]; }}
Cuando se dispare uno de los eventos devueltos por when(), el provider se cargará aunque no se haya resuelto directamente ningún servicio.
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 { // Limita boot() al registro de publicaciones 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() comprueba internamente si la configuración ya está cacheada, así que es seguro llamarlo dentro del register() de un provider diferido. Eso sí, si la configuración está cacheada, no hará nada.
Separar el registro de comandos Artisan con runningInConsole()
Como el registro de comandos solo es necesario al arrancar Artisan, protégelo con runningInConsole(). Si además quieres diferir el provider que registra los comandos, incluye las clases de comando en provides() o proporciona un provider separado dedicado a los comandos.
public function boot(): void{ if ($this->app->runningInConsole()) { $this->commands([ AnalyticsFlushCommand::class, ]); }}
Muchos de los providers del core de Laravel son diferidos. Es un diseño pensado para no cargar de forma inmediata todos los servicios que no se usan en cada petición.
Provider
Servicios que provee
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
En una aplicación con solo endpoints de API puede darse el caso de que MailServiceProvider o BroadcastServiceProvider no se carguen nunca durante una petición.