Service Providers
Com funcionen els Service Providers de Laravel: el cicle de vida, register, boot, deferred providers i creació de providers personalitzats.
Què són els Service Providers?#
Els Service Providers són el mecanisme central d'arrencada de tota l'aplicació Laravel. Cada servei que el framework ofereix:rutes, base de dades, cues, correu, validació, cache, vistes, autenticació:es registra i es configura a través d'un service provider. Quan Laravel rep una petició HTTP o executa una comanda Artisan, el primer que fa després de carregar l'autoloader és instanciar i executar tots els service providers registrats. Sense ells, Laravel seria un framework buit: no sabria com gestionar rutes, ni com connectar-se a la base de dades, ni com enviar correus.
Un service provider és, en essència, una classe amb dos mètodes principals: register() i boot(). El mètode register() serveix per vincular serveis al Service Container. El mètode boot() serveix per executar qualsevol acció que necessiti que tots els serveis ja estiguin registrats. Aquesta separació en dos passos no és arbitrària: és el que permet que els providers funcionin de manera independent i predictible, fins i tot quan depenen els uns dels altres.
Entendre els service providers és fonamental per a qualsevol desenvolupador Laravel que vulgui anar més enllà de l'ús bàsic del framework. Quan crees un paquet, necessites un provider per integrar-lo. Quan necessites configurar un servei extern, el provider és on ho fas. Quan vols modificar el comportament per defecte de Laravel (per exemple, canviar com es resolen les vistes o afegir directives Blade personalitzades), el provider és el lloc natural per fer-ho. I quan intentes depurar per què un servei no funciona com esperes, saber com funcionen els providers és el que et permetrà trobar el problema.
El cicle de vida dels providers#
El cicle de vida dels service providers segueix un ordre estricte que és crucial entendre. Quan Laravel arrenca, passa per tres fases amb els providers:
Fase 1 - Instanciació: Laravel llegeix la llista de providers registrats (a bootstrap/providers.php en Laravel 11) i crea una instància de cadascun. En aquest moment, les classes existeixen però no han fet res encara.
Fase 2 - Register: Laravel crida el mètode register() de tots els providers, un per un. Durant aquesta fase, cada provider vincularà els seus serveis al contenidor. La restricció fonamental és que dins de register() no pots accedir a serveis d'altres providers, perquè no hi ha garantia que ja s'hagin registrat. Si el MailServiceProvider intenta accedir al servei de cache durant el register(), pot fallar perquè el CacheServiceProvider potser encara no ha executat el seu register().
Fase 3 - Boot: Un cop tots els providers han completat el seu register(), Laravel crida el mètode boot() de cadascun. En aquest moment, tots els serveis del contenidor estan disponibles i pots dependre d'ells amb total seguretat. Aquí és on registres rutes, directives Blade, observers, view composers, event listeners i qualsevol configuració que necessiti accedir a altres serveis.
// Ordre d'execució:
// 1. AppServiceProvider::register()
// 2. AuthServiceProvider::register()
// 3. PaymentServiceProvider::register()
// ... (tots els register)
// 4. AppServiceProvider::boot()
// 5. AuthServiceProvider::boot()
// 6. PaymentServiceProvider::boot()
// ... (tots els boot)Aquesta separació resol un problema clàssic de les aplicacions grans: les dependències circulars entre serveis. Si el servei A depèn del servei B durant la inicialització, i el servei B depèn del servei A, amb un sistema de fase única seria impossible inicialitzar-los. Amb dues fases, tots es registren primer (fase de register()) i després tots s'inicialitzen (fase de boot()), eliminant el problema.
El mètode register#
El mètode register() té un únic propòsit: vincular coses al Service Container. No hauries de fer res més aquí: no accedir a altres serveis, no registrar event listeners, no definir rutes, no carregar configuració d'altres paquets. Només bindings purs.
namespace App\Providers;
use App\Contracts\PaymentGateway;
use App\Contracts\ShippingCalculator;
use App\Services\Payment\StripePaymentGateway;
use App\Services\Shipping\WeightBasedCalculator;
use Illuminate\Support\ServiceProvider;
class CommerceServiceProvider extends ServiceProvider
{
public function register(): void
{
// Vincular interfícies a implementacions
$this->app->singleton(PaymentGateway::class, function ($app) {
return new StripePaymentGateway(
secretKey: config('services.stripe.secret'),
currency: config('commerce.default_currency', 'EUR')
);
});
$this->app->bind(ShippingCalculator::class, function ($app) {
return new WeightBasedCalculator(
baseRate: config('commerce.shipping.base_rate'),
ratePerKg: config('commerce.shipping.rate_per_kg')
);
});
// Carregar configuració del paquet si existeix
$this->mergeConfigFrom(
__DIR__.'/../../config/commerce.php', 'commerce'
);
}
}El mètode mergeConfigFrom() és una excepció legítima dins de register(). Aquest mètode combina la configuració del teu paquet amb la configuració de l'aplicació, permetent que l'aplicació pugui sobreescriure valors per defecte. Com que config() és un servei bàsic que sempre està disponible des del principi, és segur utilitzar-lo a register().
La raó per la qual és tan important limitar register() a bindings és la predictibilitat. Si tots els providers només fan bindings a register(), l'ordre en què s'executen no importa: cada provider simplement registra els seus serveis al contenidor i no depèn de res extern. Això fa que l'aplicació sigui robusta i que afegir o treure providers no causi efectes secundaris inesperats.
El mètode boot#
El mètode boot() s'executa un cop tots els serveis han estat registrats al contenidor. Aquí pots fer tot el que necessitis: accedir a altres serveis, registrar event listeners, definir view composers, afegir directives Blade, configurar model observers, definir patrons de rutes i qualsevol altra acció d'inicialització.
namespace App\Providers;
use App\Models\Article;
use App\Models\Order;
use App\Observers\ArticleObserver;
use App\Observers\OrderObserver;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// Bindings al contenidor
}
public function boot(): void
{
// Registrar observers
Article::observe(ArticleObserver::class);
Order::observe(OrderObserver::class);
// Compartir dades amb totes les vistes
View::share('appName', config('app.name'));
View::share('currentYear', now()->year);
// Definir directives Blade personalitzades
Blade::directive('money', function ($expression) {
return "<?php echo number_format($expression, 2, ',', '.') . ' €'; ?>";
});
Blade::directive('datetime', function ($expression) {
return "<?php echo ($expression)->format('d/m/Y H:i'); ?>";
});
// Definir gates d'autorització
Gate::define('manage-articles', function ($user) {
return $user->isEditor() || $user->isAdmin();
});
// Definir patrons de validació per a rutes
Route::pattern('id', '[0-9]+');
Route::pattern('slug', '[a-z0-9-]+');
}
}Una funcionalitat elegant del mètode boot() és que pots type-hintar dependències als seus paràmetres i Laravel les resoldrà automàticament del contenidor. Això és útil quan el teu boot() necessita accedir a un servei específic:
use App\Services\GeoLocation;
use Illuminate\Routing\Router;
public function boot(Router $router, GeoLocation $geo): void
{
// $router i $geo s'injecten automàticament
$router->pattern('country', '[a-z]{2}');
View::share('defaultCountry', $geo->detectCountry());
}Aquesta injecció al mètode boot() funciona gràcies al Service Container: Laravel crida boot() a través del contenidor, que inspecciona els type-hints i resol les dependències. Això evita haver de cridar app() o resolve() manualment.
Crear un provider personalitzat#
Per crear un nou service provider, utilitza la comanda Artisan:
php artisan make:provider PaymentServiceProviderAixò crea un fitxer a app/Providers/PaymentServiceProvider.php amb l'estructura bàsica. Vegem un exemple complet amb un provider que gestiona la integració de pagaments:
namespace App\Providers;
use App\Contracts\PaymentGateway;
use App\Contracts\RefundService;
use App\Services\Payment\StripePaymentGateway;
use App\Services\Payment\StripeRefundService;
use Illuminate\Support\ServiceProvider;
class PaymentServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(PaymentGateway::class, function ($app) {
return new StripePaymentGateway(
secretKey: config('services.stripe.secret'),
webhookSecret: config('services.stripe.webhook_secret'),
currency: config('payment.default_currency', 'EUR')
);
});
$this->app->bind(RefundService::class, function ($app) {
return new StripeRefundService(
gateway: $app->make(PaymentGateway::class)
);
});
$this->mergeConfigFrom(
__DIR__.'/../../config/payment.php', 'payment'
);
}
public function boot(): void
{
// Publicar la configuració perquè l'aplicació pugui personalitzar-la
$this->publishes([
__DIR__.'/../../config/payment.php' => config_path('payment.php'),
], 'payment-config');
// Registrar les rutes del webhook de pagament
$this->loadRoutesFrom(__DIR__.'/../../routes/payment.php');
// Registrar les vistes per als correus de pagament
$this->loadViewsFrom(__DIR__.'/../../resources/views', 'payment');
}
}El provider encapsula tota la configuració del sistema de pagaments en un sol lloc. Si demà decideixes canviar de Stripe a un altre proveïdor, només has de modificar aquest provider: la resta de l'aplicació segueix funcionant perquè depèn de les interfícies PaymentGateway i RefundService, no de les implementacions concretes.
Registrar providers#
A Laravel 11, els service providers es registren al fitxer bootstrap/providers.php. Aquest fitxer retorna un array amb les classes dels providers que Laravel ha de carregar:
// bootstrap/providers.php
return [
App\Providers\AppServiceProvider::class,
App\Providers\PaymentServiceProvider::class,
App\Providers\CommerceServiceProvider::class,
];L'ordre dels providers a l'array importa per a la fase de register(): els primers de la llista es registren primer. En la majoria de casos, l'ordre no és rellevant perquè register() no hauria de dependre d'altres serveis. Però si tens un provider que necessita que un altre provider ja hagi registrat els seus bindings a register() (cosa que hauries d'evitar si és possible), l'ordre pot ser important.
En versions anteriors de Laravel (10 i anteriors), els providers es registraven a l'array providers de config/app.php. El canvi a bootstrap/providers.php en Laravel 11 simplifica la configuració i separa la infraestructura d'arrencada de la configuració general de l'aplicació.
AppServiceProvider#
L'AppServiceProvider és el provider per defecte que ve amb cada projecte Laravel. És el lloc natural per a configuracions generals de l'aplicació que no justifiquen crear un provider separat. Habitualment s'utilitza per a:
namespace App\Providers;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// Bindings generals de l'aplicació
$this->app->singleton(
\App\Contracts\Analytics::class,
\App\Services\GoogleAnalytics::class
);
}
public function boot(): void
{
// Prevenir lazy loading en desenvolupament
Model::preventLazyLoading(! app()->isProduction());
// Prevenir accés a atributs que no existeixen
Model::preventAccessingMissingAttributes(! app()->isProduction());
// Requerir que els morph maps estiguin definits
// (evita guardar noms de classe complets a la base de dades)
Model::enforceMorphMap([
'user' => User::class,
'article' => \App\Models\Article::class,
'order' => \App\Models\Order::class,
]);
// Logging de queries lentes en desenvolupament
DB::whenQueryingForLongerThan(500, function ($connection) {
logger()->warning("Query lenta detectada a la connexió {$connection->getName()}");
});
}
}L'AppServiceProvider és un bon punt de partida per a configuracions petites. Però quan la lògica creix o quan tens un domini ben definit (pagaments, enviaments, analítica), és millor crear un provider dedicat. La regla general és: si la configuració pertany a un domini de negoci específic o podria ser extreta com a paquet independent, mereix el seu propi provider.
Deferred providers#
Per defecte, tots els providers es carreguen a cada petició, fins i tot si els seus serveis no s'utilitzen. Per a providers que gestionen serveis que rarament s'usen (per exemple, una integració amb un servei de facturació que només s'utilitza en certes rutes), la càrrega immediata és un malbaratament de recursos.
Els deferred providers resolen aquest problema: el provider només es carrega quan algú demana un dels serveis que proporciona. Per crear un deferred provider, implementa la interfície DeferrableProvider i defineix el mètode provides() que retorna la llista de serveis que el provider registra:
namespace App\Providers;
use App\Contracts\InvoiceGenerator;
use App\Services\Invoicing\PdfInvoiceGenerator;
use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Support\ServiceProvider;
class InvoiceServiceProvider extends ServiceProvider implements DeferrableProvider
{
public function register(): void
{
$this->app->singleton(InvoiceGenerator::class, function ($app) {
return new PdfInvoiceGenerator(
templatePath: resource_path('views/invoices'),
storagePath: storage_path('app/invoices'),
companyName: config('app.name'),
taxRate: config('invoicing.tax_rate', 21)
);
});
}
/**
* Retorna els serveis proporcionats per aquest provider.
* El provider NOMÉS es carregarà quan algú demani un d'aquests serveis.
*/
public function provides(): array
{
return [
InvoiceGenerator::class,
];
}
}Quan Laravel arrenca, no executa el register() d'aquest provider. En lloc d'això, guarda una referència que diu "si algú demana InvoiceGenerator, carrega InvoiceServiceProvider". Quan el contenidor rep la primera petició per InvoiceGenerator, carrega el provider, executa el seu register() i retorna el servei. Totes les peticions posteriors utilitzen el servei ja registrat.
Els deferred providers milloren el rendiment en aplicacions grans amb molts serveis, però tenen una limitació: el mètode boot() d'un deferred provider no s'executa fins que el provider es carrega. Això significa que no pots registrar event listeners, directives Blade o rutes en un deferred provider i esperar que estiguin disponibles des del principi. Si el teu provider necessita fer coses al boot(), no hauria de ser deferred.
Providers per a paquets#
Quan crees un paquet Laravel (ja sigui intern per al teu equip o públic per a la comunitat), el service provider és el punt d'entrada que integra el paquet amb l'aplicació. Laravel proporciona mètodes específics per a les necessitats habituals dels paquets: publicar configuració, carregar migracions, rutes, vistes, traduccions i comandes.
Publicar configuració#
El mètode publishes() permet que els usuaris del paquet copiïn fitxers de configuració al seu projecte per personalitzar-los. El mètode mergeConfigFrom() carrega la configuració per defecte del paquet i la combina amb la configuració de l'aplicació (si existeix):
public function register(): void
{
// Carregar la configuració per defecte del paquet
$this->mergeConfigFrom(
__DIR__.'/../config/analytics.php', 'analytics'
);
}
public function boot(): void
{
// Permetre que l'aplicació publiqui la configuració
$this->publishes([
__DIR__.'/../config/analytics.php' => config_path('analytics.php'),
], 'analytics-config');
}L'usuari del paquet pot publicar la configuració amb:
php artisan vendor:publish --tag=analytics-configPublicar i carregar migracions#
Els paquets poden incloure migracions que Laravel executarà automàticament, o que l'usuari pot publicar al seu directori de migracions:
public function boot(): void
{
// Carregar migracions automàticament (sense publicar)
$this->loadMigrationsFrom(__DIR__.'/../database/migrations');
// O publicar-les perquè l'usuari pugui modificar-les
$this->publishesMigrations([
__DIR__.'/../database/migrations' => database_path('migrations'),
], 'analytics-migrations');
}El mètode loadMigrationsFrom() fa que Laravel executi les migracions directament des del directori del paquet, sense necessitat de copiar-les al projecte. Això és convenient perquè els usuaris no han de gestionar fitxers addicionals, però impedeix que puguin personalitzar les migracions. publishesMigrations() és l'alternativa quan vols permetre personalització.
Carregar rutes#
Un paquet pot definir les seves pròpies rutes (per exemple, un paquet d'administració que afegeix rutes /admin/*):
public function boot(): void
{
$this->loadRoutesFrom(__DIR__.'/../routes/analytics.php');
}Les rutes es carreguen al boot() perquè necessiten que el servei de routing ja estigui disponible. Un patró comú és condicionar la càrrega de rutes a la configuració:
public function boot(): void
{
if (config('analytics.routes_enabled', true)) {
$this->loadRoutesFrom(__DIR__.'/../routes/analytics.php');
}
}Carregar vistes#
El mètode loadViewsFrom() registra un namespace per a les vistes del paquet, permetent accedir-hi amb la sintaxi paquet::vista:
public function boot(): void
{
// Registrar les vistes del paquet
$this->loadViewsFrom(__DIR__.'/../resources/views', 'analytics');
// Permetre publicar les vistes per personalitzar-les
$this->publishes([
__DIR__.'/../resources/views' => resource_path('views/vendor/analytics'),
], 'analytics-views');
}Ara les vistes del paquet s'utilitzen amb el prefix del namespace:
return view('analytics::dashboard');
return view('analytics::reports.monthly');Si l'usuari publica les vistes i les modifica, Laravel utilitza la versió publicada per sobre de l'original del paquet. Això permet personalitzar l'aparença sense modificar els fitxers del paquet.
Carregar traduccions#
Per a paquets que necessiten suport multilingüe:
public function boot(): void
{
$this->loadTranslationsFrom(__DIR__.'/../lang', 'analytics');
$this->publishes([
__DIR__.'/../lang' => $this->app->langPath('vendor/analytics'),
], 'analytics-lang');
}Les traduccions s'utilitzen amb __('analytics::messages.dashboard_title').
Registrar comandes Artisan#
Si el paquet proporciona comandes Artisan, es registren al boot() condicionalment (només quan l'aplicació s'executa en consola):
public function boot(): void
{
if ($this->app->runningInConsole()) {
$this->commands([
\App\Console\Commands\GenerateAnalyticsReport::class,
\App\Console\Commands\PurgeOldAnalyticsData::class,
]);
}
}La comprovació runningInConsole() evita registrar les comandes quan l'aplicació gestiona peticions HTTP, cosa que millora lleugerament el rendiment.
Exemple pràctic: PaymentServiceProvider complet#
Posem tot junt en un provider complet que gestiona un sistema de pagaments amb configuració, binding d'interfícies, event listeners, directives Blade i comandes Artisan:
namespace App\Providers;
use App\Console\Commands\ProcessPendingRefunds;
use App\Console\Commands\ReconcilePayments;
use App\Contracts\PaymentGateway;
use App\Contracts\RefundService;
use App\Events\PaymentCompleted;
use App\Events\PaymentFailed;
use App\Listeners\LogPaymentActivity;
use App\Listeners\NotifyCustomerOfPayment;
use App\Listeners\UpdateOrderStatus;
use App\Services\Payment\StripePaymentGateway;
use App\Services\Payment\StripeRefundService;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;
class PaymentServiceProvider extends ServiceProvider
{
public function register(): void
{
// Combinar la configuració per defecte
$this->mergeConfigFrom(
__DIR__.'/../../config/payment.php', 'payment'
);
// Registrar la passarel·la de pagament com a singleton
$this->app->singleton(PaymentGateway::class, function ($app) {
$driver = config('payment.driver', 'stripe');
return match ($driver) {
'stripe' => new StripePaymentGateway(
secretKey: config('payment.stripe.secret'),
webhookSecret: config('payment.stripe.webhook_secret'),
currency: config('payment.currency', 'EUR')
),
default => throw new \RuntimeException(
"Driver de pagament no suportat: {$driver}"
),
};
});
// Registrar el servei de reemborsaments
$this->app->bind(RefundService::class, function ($app) {
return new StripeRefundService(
gateway: $app->make(PaymentGateway::class)
);
});
}
public function boot(): void
{
// Publicar configuració
$this->publishes([
__DIR__.'/../../config/payment.php' => config_path('payment.php'),
], 'payment-config');
// Publicar migracions
$this->publishesMigrations([
__DIR__.'/../../database/migrations' => database_path('migrations'),
], 'payment-migrations');
// Carregar rutes
$this->loadRoutesFrom(__DIR__.'/../../routes/payment.php');
// Carregar vistes per als correus de pagament
$this->loadViewsFrom(
__DIR__.'/../../resources/views', 'payment'
);
// Registrar event listeners
Event::listen(PaymentCompleted::class, [
UpdateOrderStatus::class,
NotifyCustomerOfPayment::class,
LogPaymentActivity::class,
]);
Event::listen(PaymentFailed::class, [
LogPaymentActivity::class,
]);
// Directives Blade per mostrar preus
Blade::directive('price', function ($expression) {
$currency = config('payment.currency', 'EUR');
return "<?php echo number_format(($expression) / 100, 2, ',', '.') . ' {$currency}'; ?>";
});
// Comandes Artisan
if ($this->app->runningInConsole()) {
$this->commands([
ProcessPendingRefunds::class,
ReconcilePayments::class,
]);
}
}
}Les vistes del paquet es poden utilitzar als correus:
// Utilitzar la vista del paquet
return view('payment::emails.receipt', [
'order' => $this->order,
'transaction' => $this->transaction,
]);I la directiva Blade es pot utilitzar a qualsevol vista:
{{-- Mostra "125,50 EUR" per al valor 12550 (cèntims) --}}
<span>Total: @price($order->total_cents)</span>Diferència entre AppServiceProvider i providers personalitzats#
Una pregunta habitual és: quan hauria de crear un provider nou en lloc d'afegir la lògica a l'AppServiceProvider? La resposta es basa en dos criteris: la cohesió i la reutilització.
L'AppServiceProvider és ideal per a configuració general que afecta tota l'aplicació: configurar el comportament d'Eloquent, compartir variables amb les vistes, definir macros globals, configurar el format de dates o el comportament de les respostes JSON. Si la configuració no pertany a cap domini de negoci específic, va a l'AppServiceProvider.
Un provider personalitzat és la millor opció quan la configuració pertany a un domini de negoci definit (pagaments, enviaments, facturació, analítica) o quan el conjunt de serveis podria ser extret com a paquet independent en el futur. Tenir un PaymentServiceProvider separat fa que sigui evident on està tota la configuració de pagaments, i facilita desactivar-la completament si cal (simplement traient-la de bootstrap/providers.php).
La regla pràctica és senzilla: si l'AppServiceProvider creix fins al punt que és difícil de navegar, és moment de dividir-lo en providers específics. Un AppServiceProvider de 200 línies és un senyal clar que hi ha responsabilitats barrejades que haurien de viure en providers dedicats.
// bootstrap/providers.php - una aplicació ben organitzada
return [
App\Providers\AppServiceProvider::class, // Configuració general
App\Providers\PaymentServiceProvider::class, // Domini de pagaments
App\Providers\ShippingServiceProvider::class, // Domini d'enviaments
App\Providers\AnalyticsServiceProvider::class, // Integració amb analítica
App\Providers\SearchServiceProvider::class, // Motor de cerca
];Cada provider encapsula un domini de responsabilitat clara. Quan algú nou s'incorpora a l'equip, pot entendre l'arquitectura de l'aplicació simplement mirant la llista de providers. Quan cal depurar un problema de pagaments, sap exactament on buscar. I quan cal desactivar la funcionalitat de cerca temporalment, n'hi ha prou amb comentar una línia.
Els Service Providers són l'estructura organitzativa de tota l'aplicació Laravel. Dominar-los significa dominar la capacitat d'estendre, configurar i organitzar el framework segons les necessitats del teu projecte. Si el Service Container és el cor de Laravel, els Service Providers són el sistema circulatori que el nodreix.