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
versión: 3.x