Transmiterea dependențelor

Argumentele, sau “dependențele” în terminologia DI, pot fi transmise claselor în următoarele moduri principale:

  • trecerea prin constructor
  • transmiterea prin metodă (numită setter)
  • prin stabilirea unei proprietăți
  • prin metoda, adnotarea sau atributul inject.

În continuare, vom ilustra diferitele variante cu exemple concrete.

Injectarea constructorilor

Dependențele sunt transmise ca argumente constructorului atunci când obiectul este creat:

class MyClass
{
	private Cache $cache;

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

$obj = new MyClass($cache);

Această formă este utilă pentru dependențele obligatorii de care clasa are neapărat nevoie pentru a funcționa, deoarece fără ele instanța nu poate fi creată.

Începând cu PHP 8.0, putem utiliza o formă mai scurtă de notație care este echivalentă din punct de vedere funcțional (constructor property promotion):

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

Începând cu PHP 8.1, o proprietate poate fi marcată cu un indicator readonly care declară că conținutul proprietății nu se va modifica:

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

Containerul DI transmite dependențele către constructor în mod automat, utilizând autowiring. Argumentele care nu pot fi transmise în acest mod (de exemplu, șiruri de caractere, numere, booleeni) se scriu în configurație.

Iadul Constructorilor

Termenul constructor hell se referă la o situație în care un copil moștenește dintr-o clasă părinte al cărei constructor necesită dependențe, iar copilul necesită și el dependențe. De asemenea, acesta trebuie să preia și să transmită dependențele părintelui:

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;
	}
}

Problema apare atunci când dorim să modificăm constructorul clasei BaseClass, de exemplu atunci când se adaugă o nouă dependență. Atunci trebuie să modificăm și toți constructorii copiilor. Ceea ce face ca o astfel de modificare să fie un iad.

Cum să prevenim acest lucru? Soluția este să prioritizăm compoziția în detrimentul moștenirii.

Așadar, haideți să proiectăm codul în mod diferit. Vom evita clasele abstracte Base*. În loc ca MyClass să obțină o anumită funcționalitate prin moștenirea de la BaseClass, aceasta va avea acea funcționalitate transmisă ca dependență:

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;
	}
}

Injectarea setterilor

Dependențele sunt transmise prin apelarea unei metode care le stochează într-o proprietate privată. Convenția de denumire obișnuită pentru aceste metode este de forma set*(), motiv pentru care sunt numite setters, dar, desigur, pot fi numite în orice alt mod.

class MyClass
{
	private Cache $cache;

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

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

Această metodă este utilă pentru dependențele opționale care nu sunt necesare pentru funcția clasei, deoarece nu este garantat faptul că obiectul le va primi efectiv (adică, că utilizatorul va apela metoda).

În același timp, această metodă permite ca setterul să fie apelat în mod repetat pentru a modifica dependența. Dacă acest lucru nu este de dorit, adăugați o verificare la metodă sau, începând cu PHP 8.1, marcați proprietatea $cache cu steagul 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;
	}
}

Apelarea setterului este definită în configurația containerului DI în secțiunea setup. Tot aici este utilizată trecerea automată a dependențelor prin autowiring:

services:
	-
		create: MyClass
		setup:
			- setCache

Injectarea proprietăților

Dependențele sunt trecute direct în proprietate:

class MyClass
{
	public Cache $cache;
}

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

Această metodă este considerată nepotrivită deoarece proprietatea trebuie declarată ca fiind public. Prin urmare, nu avem niciun control asupra faptului că dependența transmisă va fi de tipul specificat (acest lucru era valabil înainte de PHP 7.4) și pierdem posibilitatea de a reacționa la dependența nou atribuită cu propriul cod, de exemplu pentru a preveni modificările ulterioare. În același timp, proprietatea devine parte a interfeței publice a clasei, ceea ce poate să nu fie de dorit.

Setarea variabilei este definită în configurația containerului DI în secțiunea de configurare:

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

Injectați

În timp ce cele trei metode anterioare sunt, în general, valabile în toate limbajele orientate pe obiecte, injectarea prin metodă, adnotare sau atributul inject este specifică prezentatorilor Nette. Acestea sunt discutate într-un capitol separat.

Ce modalitate să alegeți?

  • constructorul este potrivit pentru dependențele obligatorii de care clasa are nevoie pentru a funcționa
  • setterul, pe de altă parte, este potrivit pentru dependențele opționale sau pentru dependențele care pot fi modificate
  • variabilele publice nu sunt recomandate
versiune: 3.x