Passaggio delle dipendenze

Gli argomenti, o “dipendenze” nella terminologia della DI, si possono passare alle classi nei modi principali seguenti:

  • iniezione tramite costruttore
  • iniezione tramite metodo (la cosiddetta setter injection)
  • iniezione tramite proprietà
  • con il metodo inject*() o l'attributo #[Inject]

Mostriamo ogni variante con esempi concreti.

Iniezione tramite costruttore

Le dipendenze vengono fornite come argomenti del costruttore nel momento in cui l'oggetto viene istanziato:

class MyClass
{
	private Cache $cache;

	public function __construct(Cache $cache)
	{
		$this->cache = $cache;
	}
}

$obj = new MyClass($cache);

Questo approccio è adatto alle dipendenze obbligatorie, di cui la classe ha assolutamente bisogno per funzionare, perché senza di esse l'istanza non si può creare.

Da PHP 8.0 possiamo usare una notazione più breve (constructor property promotion), funzionalmente equivalente:

// PHP 8.0
class MyClass
{
	public function __construct(
		private Cache $cache,
	) {
	}
}

Da PHP 8.1 una proprietà si può contrassegnare con il flag readonly, che dichiara che il valore della proprietà non cambierà dopo l'inizializzazione:

// PHP 8.1
class MyClass
{
	public function __construct(
		private readonly Cache $cache,
	) {
	}
}

Il container DI passa automaticamente le dipendenze al costruttore tramite l'autowiring. Gli argomenti che non si possono fornire in questo modo (per esempio stringhe, numeri, booleani) si indicano nella configurazione.

L'inferno dei costruttori

Il termine inferno dei costruttori descrive la situazione in cui una classe figlia eredita da una classe genitore il cui costruttore richiede delle dipendenze, e anche la classe figlia richiede delle dipendenze. Deve allora accettare e girare anche 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;

	// ⛔ INFERNO DEI COSTRUTTORI
	public function __construct(Cache $cache, Database $db)
	{
		parent::__construct($cache);
		$this->db = $db;
	}
}

Il problema nasce quando vogliamo cambiare il costruttore di BaseClass, per esempio quando si aggiunge una nuova dipendenza. Diventa allora necessario modificare anche tutti i costruttori delle classi figlie. Il che trasforma una modifica del genere in un inferno.

Come si può evitare? La soluzione è preferire la composizione all'ereditarietà.

Progettiamo quindi il codice diversamente. Eviteremo le classi astratte Base*. Invece che MyClass acquisisca una certa funzionalità ereditando da BaseClass, questa funzionalità le verrà 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;
	}
}

Setter injection

Le dipendenze vengono fornite chiamando un metodo che le salva in una proprietà privata. La convenzione di denominazione più diffusa per questi metodi è lo schema set*(), da cui il nome setter, ma naturalmente si possono chiamare diversamente.

class MyClass
{
	private Cache $cache;

	public function setCache(Cache $cache): void
	{
		$this->cache = $cache;
	}
}

$obj = new MyClass;
$obj->setCache($cache);

Questo approccio è adatto alle dipendenze facoltative, non essenziali al funzionamento della classe, perché non è garantito che l'oggetto riceva davvero la dipendenza (cioè che il chiamante invochi il metodo).

Allo stesso tempo questo metodo permette di chiamare ripetutamente il setter per cambiare la dipendenza. Se non è desiderabile, aggiungete un controllo nel metodo oppure, da PHP 8.1, contrassegnate la proprietà $cache con il flag readonly.

class MyClass
{
	private Cache $cache;

	public function setCache(Cache $cache): void
	{
		if (isset($this->cache)) {
			throw new RuntimeException('The dependency has already been set');
		}
		$this->cache = $cache;
	}
}

La chiamata del setter si definisce nella configurazione del container DI, nella chiave setup. Anche qui si usa la fornitura automatica delle dipendenze tramite autowiring:

services:
	-	create: MyClass
		setup:
			- setCache

Iniezione tramite proprietà

Le dipendenze vengono fornite scrivendo direttamente in una proprietà della classe:

class MyClass
{
	public Cache $cache;
}

$obj = new MyClass;
$obj->cache = $cache;

Questo metodo è considerato inadatto, perché la proprietà deve essere dichiarata public. Di conseguenza perdiamo il controllo sul fatto che la dipendenza passata sia davvero del tipo richiesto (cosa particolarmente vera prima delle dichiarazioni di tipo delle proprietà di PHP 7.4) e perdiamo la possibilità di reagire con logica personalizzata a una dipendenza appena assegnata, per esempio per impedirne la successiva modifica. Allo stesso tempo la proprietà entra a far parte dell'API pubblica della classe, cosa che potrebbe non essere voluta.

L'assegnazione alla proprietà si definisce nella configurazione del container DI, nella sezione setup:

services:
	-	create: MyClass
		setup:
			- $cache = @\Cache

Inject

Mentre i tre approcci precedenti valgono in generale in tutti i linguaggi orientati agli oggetti, l'iniezione tramite i metodi inject*() o l'attributo #[Inject] si usa di norma con i presenter di Nette, dove è attiva per impostazione predefinita; qualsiasi altro servizio può aderirvi con inject: true. Se ne parla in un capitolo a parte.

Quale metodo scegliere?

  • Il costruttore è adatto alle dipendenze obbligatorie, di cui la classe ha assolutamente bisogno per funzionare.
  • Il setter, al contrario, è adatto alle dipendenze facoltative o a quelle che potrebbe essere necessario cambiare in seguito.
  • Le proprietà pubbliche in generale non sono consigliate.
versione: 3.x