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 una funzionalità da MyClass ereditando da BaseClass, 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
versione: 3.x