Pasar dependencias

Los argumentos, o “dependencias” en la terminología DI, se pueden pasar a las clases de las siguientes formas principales:

  • pasando por constructor
  • pasando por método (llamado setter)
  • estableciendo una propiedad
  • por método, anotación o atributo inject.

A continuación ilustraremos las distintas variantes con ejemplos concretos.

Inyección de constructor

Las dependencias se pasan como argumentos al constructor cuando se crea el objeto:

class MyClass
{
	private Cache $cache;

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

$obj = new MyClass($cache);

Esta forma es útil para dependencias obligatorias que la clase necesita absolutamente para funcionar, ya que sin ellas la instancia no puede ser creada.

Desde PHP 8.0, podemos usar una forma más corta de notación que es funcionalmente equivalente (constructor property promotion):

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

A partir de PHP 8.1, una propiedad puede ser marcada con una bandera readonly que declara que el contenido de la propiedad no cambiará:

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

El contenedor DI pasa dependencias al constructor automáticamente usando autowiring. Argumentos que no se pueden pasar de esta forma (por ejemplo cadenas, números, booleanos) escribir en configuración.

Infierno constructor

El término infierno de constructores se refiere a una situación en la que un hijo hereda de una clase padre cuyo constructor requiere dependencias, y el hijo también requiere dependencias. También debe asumir y pasar las dependencias del padre:

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

El problema surge cuando queremos cambiar el constructor de la clase BaseClass, por ejemplo cuando se añade una nueva dependencia. Entonces tenemos que modificar también todos los constructores de los hijos. Lo que convierte tal modificación en un infierno.

¿Cómo evitarlo? La solución es priorizar la composición sobre la herencia.

Así que vamos a diseñar el código de forma diferente. Evitaremos las clases abstractas Base*. En lugar de que MyClass obtenga alguna funcionalidad heredando de BaseClass, tendrá esa funcionalidad pasada como una dependencia:

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

Inyección de Setter

Las dependencias se pasan llamando a un método que las almacena en una propiedad privada. La convención de nomenclatura habitual para estos métodos es de la forma set*(), que es por lo que se llaman setters, pero por supuesto pueden llamarse de cualquier otra forma.

class MyClass
{
	private Cache $cache;

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

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

Este método es útil para dependencias opcionales que no son necesarias para la función de la clase, ya que no se garantiza que el objeto las reciba realmente (es decir, que el usuario llame al método).

Al mismo tiempo, este método permite llamar al setter repetidamente para cambiar la dependencia. Si esto no es deseable, añada una comprobación al método, o a partir de PHP 8.1, marque la propiedad $cache con la bandera readonly.

class MyClass
{
	private Cache $cache;

	public function setCache(Cache $cache): void
	{
		if ($this->cache) {
			throw new RuntimeException('La dependencia ya se ha establecido');
		}
		$this->cache = $cache;
	}
}

La llamada al setter se define en la configuración del contenedor DI en sección setup. También aquí se utiliza el paso automático de dependencias mediante autowiring:

services:
	-	create: MyClass
		setup:
			- setCache

Inyección de propiedades

Las dependencias se pasan directamente a la propiedad:

class MyClass
{
	public Cache $cache;
}

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

Este método se considera inapropiado porque la propiedad debe declararse como public. Por lo tanto, no tenemos control sobre si la dependencia pasada será realmente del tipo especificado (esto era cierto antes de PHP 7.4) y perdemos la capacidad de reaccionar a la dependencia recién asignada con nuestro propio código, por ejemplo para evitar cambios posteriores. Al mismo tiempo, la propiedad pasa a formar parte de la interfaz pública de la clase, lo que puede no ser deseable.

La configuración de la variable se define en la configuración del contenedor DI en sección setup:

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

Inyectar

Mientras que los tres métodos anteriores son generalmente válidos en todos los lenguajes orientados a objetos, la inyección por método, anotación o atributo inject es específica de los presentadores Nette. Se tratan en un capítulo aparte.

¿Qué camino elegir?

  • el constructor es adecuado para dependencias obligatorias que la clase necesita para funcionar.
  • el setter, por otro lado, es adecuado para dependencias opcionales, o dependencias que se pueden cambiar.
  • las variables públicas no son recomendables.
versión: 3.x