Passaggio di dipendenze
Gli argomenti, o “dipendenze” nella terminologia DI, possono essere passati alle classi nei seguenti modi principali:
- passaggio per costruttore
- passando per un metodo (chiamato setter)
- impostando una proprietà
- con un metodo, un'annotazione o un attributo * con un'iniezione*.
Illustriamo ora le diverse varianti con esempi concreti.
Iniezione del costruttore
Le dipendenze vengono passate come argomenti al costruttore quando l'oggetto viene creato:
class MyClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
$obj = new MyClass($cache);
Questa forma è utile per le dipendenze obbligatorie di cui la classe ha assolutamente bisogno per funzionare, poiché senza di esse l'istanza non può essere creata.
Da PHP 8.0, è possibile utilizzare una forma di notazione più breve (constructor property promotion), funzionalmente equivalente:
// PHP 8.0
class MyClass
{
public function __construct(
private Cache $cache,
) {
}
}
A partire da PHP 8.1, una proprietà può essere contrassegnata da un flag readonly
che dichiara che il contenuto
della proprietà non cambierà:
// PHP 8.1
class MyClass
{
public function __construct(
private readonly Cache $cache,
) {
}
}
Il contenitore DI passa le dipendenze al costruttore automaticamente, utilizzando l'autowiring. Gli argomenti che non possono essere passati in questo modo (per esempio stringhe, numeri, booleani) vengono scritti nella configurazione.
L'inferno dei costruttori
Il termine inferno dei costruttori si riferisce a una situazione in cui un figlio eredita da una classe genitore il cui costruttore richiede delle dipendenze, e anche il figlio richiede delle dipendenze. Deve anche assumere e trasmettere le dipendenze del genitore:
abstract class BaseClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
final class MyClass extends BaseClass
{
private Database $db;
// ⛔ CONSTRUCTOR HELL
public function __construct(Cache $cache, Database $db)
{
parent::__construct($cache);
$this->db = $db;
}
}
Il problema si presenta quando si vuole modificare il costruttore della classe BaseClass
, ad esempio quando viene
aggiunta una nuova dipendenza. Allora dobbiamo modificare anche tutti i costruttori dei figli. Il che rende tale modifica un
inferno.
Come evitarlo? La soluzione è quella di privilegiare la composizione rispetto all'ereditarietà.
Quindi progettiamo il codice in modo diverso. Eviteremo le classi astratte di
Base*
. Invece di ottenere alcune funzionalità ereditando da BaseClass
, MyClass
avrà
quella funzionalità passata come dipendenza:
final class SomeFunctionality
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
final class MyClass
{
private SomeFunctionality $sf;
private Database $db;
public function __construct(SomeFunctionality $sf, Database $db) // ✅
{
$this->sf = $sf;
$this->db = $db;
}
}
Iniezione di setter
Le dipendenze vengono passate chiamando un metodo che le memorizza in una proprietà privata. La convenzione di denominazione
usuale per questi metodi è la forma set*()
, che è il motivo per cui sono chiamati setter, ma naturalmente possono
essere chiamati in qualsiasi altro modo.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
$this->cache = $cache;
}
}
$obj = new MyClass;
$obj->setCache($cache);
Questo metodo è utile per le dipendenze opzionali che non sono necessarie per il funzionamento della classe, poiché non è garantito che l'oggetto le riceva effettivamente (cioè che l'utente chiami il metodo).
Allo stesso tempo, questo metodo consente di richiamare ripetutamente il setter per modificare la dipendenza. Se ciò non è
auspicabile, si può aggiungere un controllo al metodo o, a partire da PHP 8.1, contrassegnare la proprietà $cache
con il flag readonly
.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
if ($this->cache) {
throw new RuntimeException('The dependency has already been set');
}
$this->cache = $cache;
}
}
La chiamata al setter è definita nella configurazione del contenitore DI, nella sezione setup. Anche qui il passaggio automatico delle dipendenze è usato dall'autowiring:
services:
- create: MyClass
setup:
- setCache
Iniezione di proprietà
Le dipendenze vengono passate direttamente alla proprietà:
class MyClass
{
public Cache $cache;
}
$obj = new MyClass;
$obj->cache = $cache;
Questo metodo è considerato inappropriato, perché la proprietà deve essere dichiarata come public
. Quindi, non
si ha alcun controllo sul fatto che la dipendenza passata sia effettivamente del tipo specificato (questo era vero prima di PHP
7.4) e si perde la possibilità di reagire alla nuova dipendenza assegnata con il proprio codice, ad esempio per prevenire
modifiche successive. Allo stesso tempo, la proprietà diventa parte dell'interfaccia pubblica della classe, il che potrebbe non
essere auspicabile.
L'impostazione della variabile è definita nella configurazione del contenitore DI, nella sezione setup:
services:
- create: MyClass
setup:
- $cache = @\Cache
Iniettare
Mentre i tre metodi precedenti sono generalmente validi in tutti i linguaggi orientati agli oggetti, l'iniezione tramite metodo, annotazione o attributo inject è specifica dei presentatori Nette. Sono trattati in un capitolo a parte.
Quale strada scegliere?
- il costruttore è adatto alle dipendenze obbligatorie di cui la classe ha bisogno per funzionare
- il setter, invece, è adatto per le dipendenze opzionali, o per le dipendenze che possono essere modificate
- Le variabili pubbliche non sono consigliate