Paso de dependencias
Los argumentos, o “dependencias” en la terminología de la DI, se pueden pasar a las clases de las siguientes maneras principales:
- inyección por constructor
- inyección por método (la llamada inyección por setter)
- inyección en propiedades
- mediante el método
inject*()o el atributo#[Inject]
Mostremos cada variante con ejemplos concretos.
Inyección por constructor
Las dependencias se proporcionan como argumentos del constructor en el momento de crear el objeto:
class MyClass
{
private Cache $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
}
$obj = new MyClass($cache);
Este enfoque es adecuado para las dependencias obligatorias que la clase necesita imprescindiblemente para funcionar, porque sin ellas no se puede crear la instancia.
Desde PHP 8.0 podemos usar una notación más corta (promoción de propiedades del constructor), que es funcionalmente equivalente:
// PHP 8.0
class MyClass
{
public function __construct(
private Cache $cache,
) {
}
}
Desde PHP 8.1, una propiedad se puede marcar con la bandera readonly, que declara que el valor de la propiedad no
cambiará después de la inicialización:
// 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 proporcionar así (p. ej. cadenas, números, booleanos) se indican en la configuración.
Infierno del constructor
El término infierno del constructor describe una situación en la que una clase hija hereda de una clase padre cuyo constructor requiere dependencias, y la clase hija requiere además las suyas propias. Entonces tiene que aceptar y pasar también 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;
// ⛔ INFIERNO DEL CONSTRUCTOR
public function __construct(Cache $cache, Database $db)
{
parent::__construct($cache);
$this->db = $db;
}
}
El problema surge cuando queremos cambiar el constructor de BaseClass, por ejemplo al añadir una nueva
dependencia. Entonces hay que modificar también todos los constructores de las clases hijas. Lo que convierte esa modificación
en un infierno.
¿Cómo se puede evitar? La solución es preferir la composición a la herencia.
Así que diseñamos el código de otra manera. Evitaremos las clases abstractas
Base*. En lugar de que MyClass obtenga cierta funcionalidad heredando de BaseClass, se le
pasará esa funcionalidad 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;
}
}
Inyección por setter
Las dependencias se proporcionan llamando a un método que las guarda en una propiedad privada. La convención de nombres
habitual para estos métodos es el patrón set*(), de ahí que se les llame setters, aunque naturalmente pueden
llamarse de otra manera.
class MyClass
{
private Cache $cache;
public function setCache(Cache $cache): void
{
$this->cache = $cache;
}
}
$obj = new MyClass;
$obj->setCache($cache);
Este enfoque es adecuado para las dependencias opcionales, que no son imprescindibles para el funcionamiento de la clase, porque no está garantizado que el objeto reciba realmente la dependencia (es decir, que quien lo usa llame al método).
Al mismo tiempo, este método permite llamar al setter repetidamente para cambiar la dependencia. Si eso no es deseable, añada
una comprobación dentro del método o, desde PHP 8.1, marque la propiedad $cache con la bandera
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. También aquí se usa el suministro automático de dependencias mediante autowiring:
services:
- create: MyClass
setup:
- setCache
Inyección en propiedades
Las dependencias se proporcionan escribiendo directamente en una propiedad miembro:
class MyClass
{
public Cache $cache;
}
$obj = new MyClass;
$obj->cache = $cache;
Este método se considera inadecuado, porque la propiedad miembro debe declararse public. En consecuencia perdemos
el control sobre que la dependencia pasada sea realmente del tipo requerido (esto valía sobre todo antes de que PHP
7.4 introdujera los type hints en las propiedades) y perdemos la posibilidad de reaccionar con lógica propia ante una
dependencia recién asignada, por ejemplo para impedir su modificación posterior. Al mismo tiempo, la propiedad pasa a formar
parte de la API pública de la clase, lo que puede no ser lo deseado.
La asignación a la propiedad 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 enfoques anteriores valen en general en todos los lenguajes orientados a objetos, la inyección mediante
los métodos inject*() o el atributo #[Inject] se usa normalmente con los presenters de Nette, donde
está activada de forma predeterminada; cualquier otro servicio puede activarla con inject: true. Se tratan en un capítulo aparte.
¿Qué método elegir?
- El constructor es adecuado para las dependencias obligatorias que la clase necesita imprescindiblemente para funcionar.
- El setter, en cambio, es adecuado para las dependencias opcionales o para las que quizá haya que cambiar más adelante.
- Las propiedades públicas no se recomiendan en general.