Contenitore dei servizi (Service Container)
Introduzione
Il contenitore dei servizi di Laravel è uno strumento potente per gestire le dipendenze di classe e eseguire l’iniezione delle dipendenze. L’iniezione delle dipendenze è un termine sofisticato che significa essenzialmente questo: le dipendenze di classe vengono “iniettate” nella classe tramite il costruttore o, in alcuni casi, tramite metodi “setter”.
Guardiamo un semplice esempio:
<?php namespace App\Http\Controllers; use App\Http\Controllers\Controller; use App\Repositories\UserRepository; use App\Models\User; use Illuminate\View\View; class UserController extends Controller { /** * Create a new controller instance. */ public function __construct( protected UserRepository $users, ) {} /** * Show the profile for the given user. */ public function show(string $id): View { $user = $this->users->find($id); return view('user.profile', ['user' => $user]); } }
In questo esempio, UserController ha bisogno di recuperare gli utenti da una fonte di dati. Quindi, inietteremo un servizio che è in grado di recuperare gli utenti. In questo contesto, il nostro UserRepository probabilmente utilizza Eloquent per recuperare le informazioni degli utenti dal database. Tuttavia, poiché il repository viene iniettato, siamo in grado di sostituirlo facilmente con un’altra implementazione. Siamo anche in grado di “mockare”, cioè creare un’implementazione fittizia di UserRepository durante i test dell’applicazione.
Una profonda comprensione del contenitore dei servizi di Laravel è essenziale per costruire un’applicazione potente e di grandi dimensioni, così come per contribuire al core di Laravel stesso.
Risoluzione senza configurazione
Se una classe non ha dipendenze o dipende solo da altre classi concrete (non interfacce), il container non ha bisogno di istruzioni su come risolvere quella classe. Ad esempio, puoi inserire il seguente codice nel tuo file routes/web.php:
<?php class Service { // ... } Route::get('/', function (Service $service) { die(get_class($service)); });
In questo esempio, colpendo la rotta / della tua applicazione, la classe Service viene risolta automaticamente e iniettata nel gestore della rotta. Questo è un cambiamento epocale. Significa che puoi sviluppare la tua applicazione e sfruttare l’iniezione delle dipendenze senza preoccuparti di file di configurazione ingombranti.
Fortunatamente, molte delle classi che scriverai durante la costruzione di un’applicazione Laravel ricevono automaticamente le loro dipendenze tramite il container, inclusi controllers, event listeners, middleware e altro ancora. Inoltre, puoi specificare il tipo di dipendenza nel metodo handle dei job in coda. Una volta provata la potenza dell’iniezione delle dipendenze automatica e senza configurazione, diventa impossibile sviluppare senza di essa.
Quando utilizzare il container
Grazie alla risoluzione automatica senza configurazione, spesso utilizzerai l’annotazione dei tipi per le dipendenze nelle route, nei controller, negli event listener e altrove senza mai interagire manualmente con il container. Ad esempio, potresti annotare come tipo l’oggetto Illuminate\Http\Request nella definizione della tua route in modo da poter accedere facilmente alla richiesta corrente. Anche se non è necessario interagire direttamente con il container per scrivere questo codice, il container gestisce l’iniezione di queste dipendenze dietro le quinte:
use Illuminate\Http\Request; Route::get('/', function (Request $request) { // ... });
In molti casi, grazie all’iniezione automatica delle dipendenze e alle facades, è possibile costruire applicazioni Laravel senza dover mai associare o risolvere manualmente nulla dal container. Quindi, quando si dovrebbe mai interagire manualmente con il container? Esaminiamo due situazioni.
In primo luogo, se si scrive una classe che implementa un’interfaccia e si desidera utilizzare l’annotazione del tipo di quell’interfaccia in una route o nel costruttore di una classe, è necessario indicare al container come risolvere quell’interfaccia. In secondo luogo, se si sta scrivendo un pacchetto Laravel che si intende condividere con altri sviluppatori Laravel, potrebbe essere necessario associare i servizi del proprio pacchetto nel container.
Binding
Le basi
Associazioni semplici (Simple Bindings)
Quasi tutti i binding del container dei servizi verranno registrati all’interno dei service provider, quindi la maggior parte di questi esempi mostrerà come utilizzare il container in quel contesto.
All’interno di un service provider, si ha sempre accesso al container tramite la proprietà $this->app. Possiamo registrare un binding utilizzando il metodo bind, passando il nome della classe o dell’interfaccia che desideriamo registrare insieme a una closure che restituisce un’istanza della classe:
use App\Services\Transistor; use App\Services\PodcastParser; use Illuminate\Contracts\Foundation\Application; $this->app->bind(Transistor::class, function (Application $app) { return new Transistor($app->make(PodcastParser::class)); });
È importante notare che riceviamo il container stesso come argomento del resolver. Possiamo quindi utilizzare il container per risolvere le sub-dipendenze dell’oggetto che stiamo costruendo.
Come accennato, di solito interagirete con il container all’interno dei service provider. Tuttavia, se desiderate interagire con il container al di fuori di un service provider, potete farlo tramite la facade App:
use App\Services\Transistor; use Illuminate\Contracts\Foundation\Application; use Illuminate\Support\Facades\App; App::bind(Transistor::class, function (Application $app) { // ... });
Potete utilizzare il metodo bindIf per registrare un binding nel container solo se non è già stato registrato un binding per il tipo specificato:
$this->app->bindIf(Transistor::class, function (Application $app) { return new Transistor($app->make(PodcastParser::class)); });
Non è necessario registrare classi nel container se non dipendono da nessuna interfaccia. Il container non ha bisogno di istruzioni su come costruire questi oggetti, poiché può risolverli automaticamente utilizzando la riflessione.
Collegare un Singleton (Binding A Singleton)
Il metodo singleton effettua un binding di una classe o di un’interfaccia nel container che verrà risolta una sola volta. Una volta risolto un binding singleton, la stessa istanza dell’oggetto verrà restituita nelle chiamate successive al container:
use App\Services\Transistor; use App\Services\PodcastParser; use Illuminate\Contracts\Foundation\Application; $this->app->singleton(Transistor::class, function (Application $app) { return new Transistor($app->make(PodcastParser::class)); });
Puoi utilizzare il metodo singletonIf per registrare un binding singleton nel container solo se non è già stato registrato un binding per il tipo specificato:
$this->app->singletonIf(Transistor::class, function (Application $app) { return new Transistor($app->make(PodcastParser::class)); });
Collegamento dei Singleton con ambiti (Binding Scoped Singletons)
Il metodo scoped collega una classe o un’interfaccia al contenitore in modo che venga risolta una sola volta all’interno del ciclo di vita di una richiesta o di un job di Laravel. Anche se questo metodo è simile al metodo singleton, le istanze registrate usando il metodo scoped verranno eliminate ogni volta che l’applicazione Laravel avvia un nuovo “ciclo di vita”, ad esempio quando un worker di Laravel Octane elabora una nuova richiesta o quando un worker della coda di Laravel elabora un nuovo job.
use App\Services\Transistor; use App\Services\PodcastParser; use Illuminate\Contracts\Foundation\Application; $this->app->scoped(Transistor::class, function (Application $app) { return new Transistor($app->make(PodcastParser::class)); });
Collegamento di istanze (Binding Instances)
Puoi anche legare un’istanza di un oggetto esistente nel container utilizzando il metodo instance. L’istanza fornita verrà sempre restituita nelle chiamate successive al container.
use App\Services\Transistor; use App\Services\PodcastParser; $service = new Transistor(new PodcastParser); $this->app->instance(Transistor::class, $service);
Legare le interfacce alle implementazioni (Binding Interfaces To Implementations)
Un’opzione molto potente del service container è la sua capacità di associare un’interfaccia a un’implementazione specifica. Ad esempio, supponiamo di avere un’interfaccia chiamata EventPusher e un’implementazione chiamata RedisEventPusher. Una volta che abbiamo codificato l’implementazione RedisEventPusher per questa interfaccia, possiamo registrarla nel service container nel seguente modo:
use App\Contracts\EventPusher; use App\Services\RedisEventPusher; $this->app->bind(EventPusher::class, RedisEventPusher::class);
Questa istruzione dice al container che dovrebbe utilizzare RedisEventPusher quando una classe necessita di un’implementazione di EventPusher. Ora possiamo dichiarare il type hint dell’interfaccia EventPusher nel costruttore di una classe che viene risolta dal container. Ricorda che i controller, gli event listener, i middleware e vari altri tipi di classi all’interno delle applicazioni Laravel vengono sempre risolti utilizzando il container.
use App\Contracts\EventPusher; /** * Create a new class instance. */ public function __construct( protected EventPusher $pusher ) {}
Legame contestuale (Contextual Binding)
A volte può capitare di avere due classi che utilizzano la stessa interfaccia, ma si desidera iniettare implementazioni diverse in ciascuna classe. Ad esempio, due controller possono dipendere da diverse implementazioni del contratto Illuminate\Contracts\Filesystem\Filesystem. Laravel fornisce un’interfaccia semplice e fluente per definire questo comportamento:
use App\Http\Controllers\PhotoController; use App\Http\Controllers\UploadController; use App\Http\Controllers\VideoController; use Illuminate\Contracts\Filesystem\Filesystem; use Illuminate\Support\Facades\Storage; $this->app->when(PhotoController::class) ->needs(Filesystem::class) ->give(function () { return Storage::disk('local'); }); $this->app->when([VideoController::class, UploadController::class]) ->needs(Filesystem::class) ->give(function () { return Storage::disk('s3'); });
Legami primitivi (Binding Primitives)
A volte può capitare di avere una classe che riceve delle dipendenze di altre classi, ma ha anche bisogno di un valore primitivo come ad esempio un intero. È possibile utilizzare facilmente il legame contestuale per iniettare qualsiasi valore di cui la classe ha bisogno:
use App\Http\Controllers\UserController; $this->app->when(UserController::class) ->needs('$variableName') ->give($value);
$this->app->when(ReportAggregator::class) ->needs('$reports') ->giveTagged('reports');
Se hai bisogno di iniettare un valore da uno dei file di configurazione della tua applicazione, puoi utilizzare il metodo giveConfig:
$this->app->when(ReportAggregator::class) ->needs('$timezone') ->giveConfig('app.timezone');
Vincolare i variadici tipizzati (Binding Typed Variadics)
Occasionalmente, potresti avere una classe che riceve un array di oggetti tipizzati utilizzando un argomento del costruttore variadico:
<?php use App\Models\Filter; use App\Services\Logger; class Firewall { /** * The filter instances. * * @var array */ protected $filters; /** * Create a new class instance. */ public function __construct( protected Logger $logger, Filter ...$filters, ) { $this->filters = $filters; } }
Utilizzando il binding contestuale, puoi risolvere questa dipendenza fornendo al metodo give
una funzione di chiusura che restituisce un array di istanze di Filter
risolte:
$this->app->when(Firewall::class) ->needs(Filter::class) ->give(function (Application $app) { return [ $app->make(NullFilter::class), $app->make(ProfanityFilter::class), $app->make(TooLongFilter::class), ]; });
Per comodità, puoi anche semplicemente fornire un array di nomi di classe da risolvere attraverso il container ogni volta che Firewall
ha bisogno di istanze di Filter
:
$this->app->when(Firewall::class) ->needs(Filter::class) ->give([ NullFilter::class, ProfanityFilter::class, TooLongFilter::class, ]);
Dipendenze dei tag variadici (Variadic Tag Dependencies)
A volte una classe può avere una dipendenza variadica che è type-hinted come una data classe (Report ...$reports
). Utilizzando i metodi needs
e giveTagged
, è possibile iniettare facilmente tutte le associazioni del container con quel tag per la dipendenza specificata:
$this->app->when(ReportAggregator::class) ->needs(Report::class) ->giveTagged('reports');
Etichettatura (Tagging)
Occasionalmente, potresti dover risolvere tutti gli elementi di una determinata “categoria” di associazioni. Ad esempio, potresti stai costruendo un analizzatore di report che riceve un array di implementazioni di interfaccia Report diverse. Dopo aver registrato le implementazioni di Report, puoi assegnare loro un tag utilizzando il metodo tag
:
$this->app->bind(CpuReport::class, function () { // ... }); $this->app->bind(MemoryReport::class, function () { // ... }); $this->app->tag([CpuReport::class, MemoryReport::class], 'reports');
Una volta che i servizi sono stati contrassegnati con un tag, puoi facilmente risolverli tutti utilizzando il metodo tagged
del container:
$this->app->bind(ReportAnalyzer::class, function (Application $app) { return new ReportAnalyzer($app->tagged('reports')); });
Estendere i legami (Extending Bindings)
Il metodo extend
consente la modifica dei servizi risolti. Ad esempio, quando un servizio viene risolto, è possibile eseguire del codice aggiuntivo per decorare o configurare il servizio. Il metodo extend
accetta due argomenti: la classe del servizio che si sta estendendo e una chiusura che deve restituire il servizio modificato. La chiusura riceve come argomenti il servizio in fase di risoluzione e l’istanza del container.
$this->app->extend(Service::class, function (Service $service, Application $app) { return new DecoratedService($service); });
Risoluzione
Il metodo make
È possibile utilizzare il metodo make
per risolvere un’istanza di classe dal container. Il metodo make
accetta il nome della classe o dell’interfaccia che si desidera risolvere:
use App\Services\Transistor; $transistor = $this->app->make(Transistor::class);
Se alcune delle dipendenze della tua classe non possono essere risolte tramite il container, puoi iniettarle passandole come un array associativo nel metodo makeWith
. Ad esempio, potremmo passare manualmente l’argomento del costruttore $id
richiesto dal servizio Transistor:
use App\Services\Transistor; $transistor = $this->app->makeWith(Transistor::class, ['id' => 1]);
Il metodo bound
può essere utilizzato per determinare se una classe o un’interfaccia è stata esplicitamente associata nel container:
if ($this->app->bound(Transistor::class)) { // ... }
Se ti trovi al di fuori di un service provider in una parte del tuo codice che non ha accesso alla variabile $app
, puoi utilizzare la facade App
o la funzione helper app
per risolvere un’istanza di classe dal container:
use App\Services\Transistor; use Illuminate\Support\Facades\App; $transistor = App::make(Transistor::class); $transistor = app(Transistor::class);
Se desideri che l’istanza stessa del container di Laravel venga iniettata in una classe che viene risolta dal container, puoi dichiarare come tipo di ingresso della classe il tipo Illuminate\Container\Container nel costruttore della classe:
use Illuminate\Container\Container; /** * Create a new class instance. */ public function __construct( protected Container $container ) {}
Iniezione automatica (Automatic Injection)
In alternativa, ed è importante sottolinearlo, puoi dichiarare come tipo di ingresso della dipendenza nel costruttore di una classe che viene risolta dal container, incluse i controller, gli event listener, i middleware e altro ancora. Inoltre, puoi dichiarare come tipo di ingresso le dipendenze nel metodo handle
dei job messi in coda. Nella pratica, è così che la maggior parte degli oggetti dovrebbe essere risolta dal container.
Ad esempio, puoi dichiarare come tipo di ingresso nel costruttore di un controller un repository definito dalla tua applicazione. Il repository verrà automaticamente risolto e iniettato nella classe:
<?php namespace App\Http\Controllers; use App\Repositories\UserRepository; use App\Models\User; class UserController extends Controller { /** * Create a new controller instance. */ public function __construct( protected UserRepository $users, ) {} /** * Show the user with the given ID. */ public function show(string $id): User { $user = $this->users->findOrFail($id); return $user; } }
Invocazione e iniezione del metodo
A volte potresti desiderare di invocare un metodo su un’istanza di un oggetto consentendo al container di iniettare automaticamente le dipendenze di quel metodo. Ad esempio, dato il seguente classe:
<?php namespace App; use App\Repositories\UserRepository; class UserReport { /** * Generate a new user report. */ public function generate(UserRepository $repository): array { return [ // ... ]; } }
Puoi invocare il metodo generate
tramite il container in questo modo:
use App\UserReport; use Illuminate\Support\Facades\App; $report = App::call([new UserReport, 'generate']);
Il metodo call
accetta qualsiasi callable di PHP. Puoi anche utilizzare il metodo call
del container per invocare una closure iniettando automaticamente le sue dipendenze:
use App\Repositories\UserRepository; use Illuminate\Support\Facades\App; $result = App::call(function (UserRepository $repository) { // ... });
Container Eventi
Il service container genera un evento ogni volta che risolve un oggetto. Puoi ascoltare questo evento utilizzando il metodo resolving
:
use App\Services\Transistor; use Illuminate\Contracts\Foundation\Application; $this->app->resolving(Transistor::class, function (Transistor $transistor, Application $app) { // Called when container resolves objects of type "Transistor"... }); $this->app->resolving(function (mixed $object, Application $app) { // Called when container resolves object of any type... });
Come puoi vedere, l’oggetto che viene risolto verrà passato alla callback, consentendoti di impostare eventuali proprietà aggiuntive sull’oggetto prima che venga fornito al suo utilizzatore.
PSR-11
Il service container di Laravel implementa l’interfaccia PSR-11. Pertanto, puoi fare il type-hint dell’interfaccia del container PSR-11 per ottenere un’istanza del container di Laravel.
use App\Services\Transistor; use Psr\Container\ContainerInterface; Route::get('/', function (ContainerInterface $container) { $service = $container->get(Transistor::class); // ... });
Viene generata un’eccezione se l’identificatore specificato non può essere risolto. L’eccezione sarà un’istanza di Psr\Container\NotFoundExceptionInterface se l’identificatore non è mai stato registrato. Se l’identificatore è stato registrato ma non è stato possibile risolverlo, verrà generata un’istanza di Psr\Container\ContainerExceptionInterface.