Dipendency Injection
Alcuni dei pattern più importanti di Laravel, oltre al già introdotto Facade Pattern, sono l'Inversion of Control e la Dependecy Injection. L'Inversion of Control prevede un'inversione rispetto al classico modello di definizione delle librerie e dipendenze usabili da un particolare componente: non è piu il componente stesso a istanziare e gestire eventuali dipendenze, ma esisterà un componente globale a livello applicativo che fornirà le dipendenze.
La Dependency Injection è un implementazione dell'Inversion of Control che prevede il passaggio di dipendenze attraverso i costruttori delle classi o tramite metodi setter
dedicati. Una delle caratteristiche comuni a tutti i framework che implementano questi pattern, anche in linguaggi diversi da PHP, è il concetto di "Container", un raccoglitore di componenti che si occupa di creare le relazioni di dipendenza tra di essi. Il Service Container di Laravel è appunto questo contenitore di classi dal quale sarà possibile utilizzare le dipendenze.
Cerchiamo di capire meglio tramite un esempio contestualizzato alla nostra applicazione. Supponiamo di voler introdurre un sistema di prenotazione di libri che prevede delle logiche particolari di disponibilità; per esempio un libro potrebbe essere disponibile ad un determinato utente solamente se non c'è nessun altro utente in "coda" o se è la prima volta che lo richiede. A prescindere dall'implementazione, una buona pratica è quella di isolare questa logica in una classe dedicata, in modo da poterla riutilizzare in diversi controller.
A questo scopo creiamo un servizio chiamato BookAvailabilityManager
che implementa un particolare metodo isAvailable
. Dato che l'argomento è la Dependency Injection non ci occuperemo della sua implementazione, diamo solamente per assodato che la classe esista e funzioni correttamente. Abbiamo però bisogno di utilizzare questo servizio all'interno del nostro BookController
:
class BookController extends Controller {
public function __construct(Biblios\Service\BookAvailabilityManager $bookAvailabilityManager)
{
$this->bookAvailabilityManager = $bookAvailabilityManager;
}
public function showIfBookIsAvailable(Book $book)
{
$isAvailable = $this->bookAvailabilityManager->isAvailable($book);
[...]
}
}
In questo piccolo esempio, abbiamo definito un costruttore per il nostro controller che si aspetta di ricevere come parametro il nostro BookAvailabilityManager. In questo caso sarà Laravel stesso ad accorgersi che abbiamo definito una dipendenza e a passare automaticamente un'istanza durante la creazione del BookController
.
Binding
Per poter utilizzare la Dependecy Injection, oltre alla definizione inevitabile delle nostre classi è necessario creare dei binding all'interno del Service Container per istruire Laravel su quali componenti devono essere gestiti a livello di framework. La creazione dei binding è triviale e viene effettuata all'interno dei service provider (argomento del prossimo capitolo). Esistono tre tipologie di binding.
La prima è il binding semplice che permette di definire una closure per istanziare una particolare classe. In questo caso, ogni volta che una classe richiederà il componente registrato la closure verrà invocata e il suo risultato verrà iniettato tramite costruttore:
$this->app->bind('Biblios\Service\BookAvailabilityManager', function ($app) {
return new Biblios\Service\BookAvailabilityManager();
});
La seconda è il binding singleton, simile al binding semplice tranne per il fatto che la closure viene invocata solamente una volta e, ad eventuali richieste successive del componente, verrà iniettata sempre la stessa istanza:
$this->app->singleton('Biblios\Service\BookAvailabilityManager', function ($app) {
return new Biblios\Service\BookAvailabilityManager();
});
Infine abbiamo il binding di un'istanza che, rispetto alle due precedenti modalità, permette di aggiungere al Service Container una particolare istanza di una classe e non una closure.
$bam = new Biblios\Service\BookAvailabilityManager();
$this->app->instance('Biblios\Service\BookAvailabilityManager', $bam);
Risolvere le dipendenze
Come abbiamo già visto nell'esempio iniziale, il miglior modo per risolvere una dipendenza è tramite il type-hinting nel costruttore del nostro componente, sia esso un controller, un middleware o un event listener. E' altresì possibile accedere ai componenti presenti nel Service Container tramite API dedicate sfruttando il metodo make
:
$bam = $this->app->make('Biblios\Service\BookAvailabilityManager');
o utilizzare il Service Container come una array associativo PHP:
$bam = $this->app['Biblios\Service\BookAvailabilityManager'];
Anche secondo gli sviluppatori di Laravel, la modalità di inject tramite costruttore è da preferire a queste ultime.