Autowiring
El autowiring es una gran característica que pasa automáticamente al constructor y a otros métodos los servicios necesarios, de modo que no tenemos que indicarlos explícitamente. Le ahorrará mucho tiempo.
Gracias a él podemos omitir la gran mayoría de los argumentos al escribir las definiciones de los servicios. En lugar de:
services:
articles: Model\ArticleRepository(@database, @cache.storage)
Basta con escribir:
services:
articles: Model\ArticleRepository
El autowiring se guía por los tipos, así que para que funcione la clase ArticleRepository debe estar definida
más o menos así:
namespace Model;
class ArticleRepository
{
public function __construct(\PDO $db, \Nette\Caching\Storage $storage)
{}
}
El autowiring nunca usa los nombres de los servicios. Se guía únicamente por el sistema de tipos de PHP, así que sabe también que una clase satisface las interfaces que implementa y las clases de las que hereda. Gracias a eso, el nombre de un servicio es solo un identificador auxiliar y renombrarlo no romperá nada en la aplicación.
Para poder usar el autowiring debe haber en el contenedor exactamente un servicio de cada tipo. Si hubiera más, el autowiring no sabría cuál pasar y lanzaría una excepción:
services:
mainDb: PDO(%dsn%, %user%, %password%)
tempDb: PDO('sqlite::memory:')
articles: Model\ArticleRepository # LANZA UNA EXCEPCIÓN, coinciden mainDb y tempDb
Una solución es saltarse el autowiring e indicar explícitamente el nombre del servicio (p. ej.
articles: Model\ArticleRepository(@mainDb)). Un enfoque más cómodo es, sin embargo, desactivar el autowiring para uno de los servicios o preferir un servicio sobre los demás.
Desactivar el autowiring
Podemos desactivar el autowiring de un servicio con la opción autowired: false:
services:
mainDb: PDO(%dsn%, %user%, %password%)
tempDb:
create: PDO('sqlite::memory:')
autowired: false # el servicio tempDb queda excluido del autowiring
articles: Model\ArticleRepository # por eso pasa mainDb al constructor
El servicio articles no lanzará una excepción por haber dos servicios PDO coincidentes
(mainDb y tempDb) disponibles para el constructor, porque solo tiene en cuenta el servicio
mainDb.
El autowiring también se puede desactivar globalmente para tipos enteros con la opción de configuración di › excluded, que enumera los tipos
(y sus descendientes) que nunca deben autoconectarse.
La configuración del autowiring en Nette se diferencia de la de Symfony. En Symfony, autowire: false
significa que el autowiring no debe usarse para los argumentos del constructor del servicio. En Nette, el autowiring se aplica a
los argumentos del constructor y a cualquier otro método invocado a través del contenedor (como la inyección por setter). La
opción autowired: false impide que el contenedor pase automáticamente esta instancia del servicio como dependencia
a otros servicios.
Preferencia de autowiring
Si tenemos varios servicios del mismo tipo e indicamos la opción autowired en uno de ellos, ese servicio pasa a
ser el preferido:
services:
mainDb:
create: PDO(%dsn%, %user%, %password%)
autowired: PDO # pasa a ser el preferido
tempDb:
create: PDO('sqlite::memory:')
articles: Model\ArticleRepository
El servicio articles no lanzará una excepción por haber varios servicios PDO coincidentes
(mainDb y tempDb), sino que usará el preferido, que es mainDb.
Colección de servicios
El autowiring también puede pasar arrays de servicios de un tipo concreto. Como PHP no admite de forma nativa indicar el tipo
de los elementos de un array en los type hints, hay que complementar el type hint array con un comentario phpDoc que
indique el tipo de los elementos, del estilo ClassName[]:
namespace Model;
class ShipManager
{
/**
* @param Shipper[] $shippers
*/
public function __construct(array $shippers)
{}
}
El contenedor DI pasa entonces automáticamente un array de los servicios que corresponden al tipo indicado. Omite los servicios que tienen el autowiring desactivado y nunca incluye en su propia colección el servicio que se está creando. A diferencia de lo que ocurre al pasar un servicio individual, aquí no tienen ningún efecto el estrechamiento del autowiring a un tipo concreto ni marcar un servicio como preferido: el array contiene siempre todos los servicios del tipo dado.
El tipo del comentario también puede tener la forma array<int, Class> o list<Class>. Si
no puede controlar la forma del comentario phpDoc, puede pasar el array de servicios directamente en la configuración con typed().
Argumentos escalares
El autowiring solo funciona para objetos y arrays de objetos. Los argumentos escalares (p. ej. cadenas, números, booleanos) hay que indicarlos en la configuración. Una alternativa es crear un objeto de ajustes que encapsule el valor escalar (o varios valores). Ese objeto se puede pasar después mediante autowiring.
class MySettings
{
public function __construct(
// readonly se puede usar desde PHP 8.1
public readonly bool $value,
)
{}
}
Lo registra como servicio añadiéndolo a la configuración:
services:
- MySettings('any value')
Las demás clases pueden pedirlo entonces mediante autowiring.
Dependencias opcionales
Si un parámetro del constructor o de un método tiene valor por defecto y en el contenedor no existe ningún servicio del tipo requerido, el autowiring no lanza una excepción: simplemente se salta el argumento, así que se usa el valor por defecto. Así se declaran las dependencias opcionales:
class Foo
{
public function __construct(
private ?Logger $logger = null,
) {}
}
En cambio, en un parámetro sin valor por defecto, la ausencia del servicio provoca siempre una excepción.
Estrechar el autowiring
En los servicios concretos, el autowiring se puede estrechar a clases o interfaces determinadas.
Normalmente, el autowiring pasa un servicio a todos los parámetros de método cuyo tipo coincida con el del servicio. Estrechar significa que establecemos condiciones que los tipos indicados en los parámetros de los métodos deben cumplir para que se les pase el servicio.
Veamos un ejemplo:
class ParentClass
{}
class ChildClass extends ParentClass
{}
class ParentDependent
{
function __construct(ParentClass $obj)
{}
}
class ChildDependent
{
function __construct(ChildClass $obj)
{}
}
Si los registráramos todos como servicios, el autowiring fallaría:
services:
parent: ParentClass
child: ChildClass
parentDep: ParentDependent # LANZA UNA EXCEPCIÓN, coinciden los servicios parent y child
childDep: ChildDependent # el autowiring pasa el servicio child al constructor
El servicio parentDep lanza la excepción Multiple services of type ParentClass found: child, parent,
porque tanto el servicio parent como el child encajan en su constructor y el autowiring no puede decidir
cuál elegir.
Para el servicio child podemos, por tanto, estrechar su autowiring al tipo ChildClass:
services:
parent: ParentClass
child:
create: ChildClass
autowired: ChildClass # también se puede escribir como 'autowired: self'
parentDep: ParentDependent # el autowiring pasa el servicio parent al constructor
childDep: ChildDependent # el autowiring pasa el servicio child al constructor
Ahora, al constructor del servicio parentDep se le pasa el servicio parent, porque es el único
objeto coincidente. El servicio child ya no se le pasa por autowiring. Sí, el servicio child sigue
siendo del tipo ParentClass, pero la condición de estrechamiento autowired: ChildClass significa que
solo se pasará a parámetros tipados explícitamente como ChildClass (o sus subtipos). Como
ParentDependent requiere ParentClass, el servicio child ya no se considera candidato para
el autowiring ahí.
Para el servicio child, autowired: ChildClass también se podría escribir como
autowired: self, porque self es un marcador de posición para la clase del servicio actual.
En la clave autowired también es posible indicar varias clases o interfaces como array:
autowired: [ParentClass, FooInterface]
Probemos a añadir interfaces al ejemplo:
interface FooInterface
{}
interface BarInterface
{}
class ParentClass implements FooInterface
{}
class ChildClass extends ParentClass implements BarInterface
{}
class FooDependent
{
function __construct(FooInterface $obj)
{}
}
class BarDependent
{
function __construct(BarInterface $obj)
{}
}
class ParentDependent
{
function __construct(ParentClass $obj)
{}
}
class ChildDependent
{
function __construct(ChildClass $obj)
{}
}
Si no restringimos el servicio child de ninguna manera, encajará en los constructores de todas las clases
FooDependent, BarDependent, ParentDependent y ChildDependent, y el autowiring
lo pasará a todos.
Sin embargo, si estrechamos su autowiring a ChildClass con autowired: ChildClass (o
self), el autowiring lo pasará solo al constructor de ChildDependent, porque este requiere un argumento
del tipo ChildClass y se cumple que ChildClass es del tipo ChildClass. Ninguno de
los tipos requeridos por los demás parámetros es ChildClass ni un subtipo suyo, así que el servicio no se
les pasa.
Si lo restringimos a ParentClass con autowired: ParentClass, el autowiring lo pasará de nuevo al
constructor de ChildDependent (porque el ChildClass requerido es un subtipo de ParentClass)
y ahora también al constructor de ParentDependent, porque el tipo requerido ParentClass también es
adecuado.
Si lo restringimos a FooInterface, se seguirá autoconectando a ParentDependent (el
ParentClass requerido es un subtipo de FooInterface) y a ChildDependent, y además al
constructor de FooDependent, pero no a BarDependent, porque BarInterface no es un subtipo
de FooInterface.
services:
child:
create: ChildClass
autowired: FooInterface
fooDep: FooDependent # el autowiring pasa el servicio child al constructor
barDep: BarDependent # LANZA UNA EXCEPCIÓN, no coincide ningún servicio
parentDep: ParentDependent # el autowiring pasa el servicio child al constructor
childDep: ChildDependent # el autowiring pasa el servicio child al constructor