Paso de dependencias

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

  • paso por constructor
  • paso por método (llamado setter)
  • asignación a variable
  • mediante método, anotación o atributo inject

Ahora mostraremos las variantes individuales con ejemplos concretos.

Paso por constructor

Las dependencias se pasan en el momento de la creación del objeto como argumentos del constructor:

class MyClass
{
	private Cache $cache;

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

$obj = new MyClass($cache);

Esta forma es adecuada para dependencias obligatorias que la clase necesita indispensablemente para su función, ya que sin ellas no se podrá crear la instancia.

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

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

Desde PHP 8.1, se puede marcar la variable con el flag readonly, que declara que el contenido de la variable ya no cambiará:

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

El contenedor DI pasa las dependencias al constructor automáticamente mediante autowiring. Los argumentos que no se pueden pasar de esta manera (por ejemplo, cadenas, números, booleanos) se escriben en la configuración.

Constructor hell

El término constructor hell se refiere a la situación en la que un descendiente hereda de una clase padre cuyo constructor requiere dependencias, y al mismo tiempo el descendiente requiere dependencias. Además, debe recibir y pasar también las 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 en el momento en que queremos cambiar el constructor de la clase BaseClass, por ejemplo, cuando se agrega una nueva dependencia. Entonces es necesario modificar también todos los constructores de los descendientes. Lo que convierte tal modificación en un infierno.

¿Cómo prevenir esto? La solución es dar preferencia a la composición sobre la herencia.

Es decir, diseñaremos el código de manera diferente. Evitaremos las clases abstractas Base*. En lugar de que MyClass obtenga cierta funcionalidad heredando de BaseClass, dejará que esta funcionalidad se le pase como 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;
	}
}

Paso por setter

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

class MyClass
{
	private Cache $cache;

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

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

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

Al mismo tiempo, este método permite llamar al setter repetidamente y así cambiar la dependencia. Si esto no es deseable, agregamos una verificación al método, o desde PHP 8.1 marcamos la propiedad $cache con el 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 llamada al setter se define en la configuración del contenedor DI en la clave setup. Aquí también se utiliza el paso automático de dependencias mediante autowiring:

services:
	-	create: MyClass
		setup:
			- setCache

Asignación a variable

Las dependencias se pasan escribiendo directamente en la variable miembro:

class MyClass
{
	public Cache $cache;
}

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

Este método se considera inadecuado porque la variable miembro debe declararse como public. Y, por lo tanto, no tenemos control sobre si la dependencia pasada será realmente del tipo dado (válido antes de PHP 7.4) y perdemos la posibilidad de reaccionar a la dependencia recién asignada con nuestro propio código, por ejemplo, para evitar cambios posteriores. Al mismo tiempo, la variable se convierte en parte de la interfaz pública de la clase, lo que puede no ser deseable.

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

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

Inject

Mientras que los tres métodos anteriores son válidos en general en todos los lenguajes orientados a objetos, la inyección mediante método, anotación o atributo inject es específica puramente para los presenters en Nette. Se tratan en un capítulo separado.

¿Qué método elegir?

  • el constructor es adecuado para dependencias obligatorias que la clase necesita indispensablemente para su función
  • el setter, por el contrario, es adecuado para dependencias opcionales, o dependencias que se puedan cambiar más adelante
  • las variables públicas no son adecuadas
versión: 3.x