Autowiring

Autowiring es una gran característica que puede pasar automáticamente los servicios requeridos al constructor y otros métodos, por lo que no tenemos que escribirlos en absoluto. Le ahorrará mucho tiempo.

Gracias a esto, podemos omitir la gran mayoría de los argumentos al escribir definiciones de servicios. En lugar de:

services:
	articles: Model\ArticleRepository(@database, @cache.storage)

Simplemente escriba:

services:
	articles: Model\ArticleRepository

Autowiring se guía por tipos, por lo que para que funcione, la clase ArticleRepository debe definirse de la siguiente manera:

namespace Model;

class ArticleRepository
{
	public function __construct(\PDO $db, \Nette\Caching\Storage $storage)
	{}
}

Para poder usar autowiring, debe haber exactamente un servicio para cada tipo en el contenedor. Si hubiera más, 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  # LANZARÁ EXCEPCIÓN, coinciden mainDb y tempDb

La solución sería omitir autowiring y especificar explícitamente el nombre del servicio (es decir, articles: Model\ArticleRepository(@mainDb)). Pero es más inteligente desactivar el autowiring de uno de los servicios, o dar preferencia al primer servicio.

Desactivación de autowiring

Podemos desactivar el autowiring de un servicio usando la opción autowired: no:

services:
	mainDb: PDO(%dsn%, %user%, %password%)

	tempDb:
		create: PDO('sqlite::memory:')
		autowired: false               # el servicio tempDb está excluido de autowiring

	articles: Model\ArticleRepository  # por lo tanto, pasa mainDb al constructor

El servicio articles no lanzará una excepción porque existen dos servicios compatibles de tipo PDO (es decir, mainDb y tempDb) que se pueden pasar al constructor, ya que solo ve el servicio mainDb.

La configuración de autowiring en Nette funciona de manera diferente que en Symfony, donde la opción autowire: false indica que no se debe usar autowiring para los argumentos del constructor del servicio dado. En Nette, autowiring siempre se usa, ya sea para los argumentos del constructor o cualquier otro método. La opción autowired: false indica que la instancia del servicio dado no debe pasarse a ningún lugar mediante autowiring.

Preferencia de autowiring

Si tenemos varios servicios del mismo tipo y especificamos la opción autowired para uno de ellos, este servicio se convierte en el preferido:

services:
	mainDb:
		create: PDO(%dsn%, %user%, %password%)
		autowired: PDO    # se convierte en el preferido

	tempDb:
		create: PDO('sqlite::memory:')

	articles: Model\ArticleRepository

El servicio articles no lanzará una excepción porque existen dos servicios compatibles de tipo PDO (es decir, mainDb y tempDb), sino que utilizará el servicio preferido, es decir, mainDb.

Array de servicios

Autowiring también puede pasar arrays de servicios de un tipo específico. Dado que en PHP no se puede escribir nativamente el tipo de los elementos del array, es necesario, además del tipo array, agregar un comentario phpDoc con el tipo del elemento en el formato ClassName[]:

namespace Model;

class ShipManager
{
	/**
	 * @param Shipper[] $shippers
	 */
	public function __construct(array $shippers)
	{}
}

El contenedor DI luego pasa automáticamente un array de servicios que coinciden con el tipo dado. Omite los servicios que tienen autowiring desactivado.

El tipo en el comentario también puede tener el formato array<int, Class> o list<Class>. Si no puede influir en la forma del comentario phpDoc, puede pasar el array de servicios directamente en la configuración usando typed().

Argumentos escalares

Autowiring solo puede inyectar objetos y arrays de objetos. Los argumentos escalares (por ejemplo, cadenas, números, booleanos) los escribimos en la configuración. Una alternativa es crear un objeto de configuración, que encapsula el valor escalar (o múltiples valores) en forma de objeto, y este luego se puede pasar nuevamente mediante autowiring.

class MySettings
{
	public function __construct(
		// readonly se puede usar desde PHP 8.1
		public readonly bool $value,
	)
	{}
}

Lo convierte en un servicio agregándolo a la configuración:

services:
	- MySettings('any value')

Todas las clases lo solicitarán luego mediante autowiring.

Restricción de autowiring

Para servicios individuales, autowiring se puede restringir solo a ciertas clases o interfaces.

Normalmente, autowiring pasa el servicio a cada parámetro del método cuyo tipo coincide con el servicio. La restricción significa que establecemos condiciones que deben cumplir los tipos especificados en los parámetros del método para que se les pase el servicio.

Lo mostraremos con 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, autowiring fallaría:

services:
	parent: ParentClass
	child: ChildClass
	parentDep: ParentDependent  # LANZARÁ EXCEPCIÓN, coinciden los servicios parent y child
	childDep: ChildDependent    # autowiring pasa el servicio child al constructor

El servicio parentDep lanzará una excepción Multiple services of type ParentClass found: parent, child, porque ambos servicios parent y child encajan en su constructor, y autowiring no puede decidir cuál elegir.

Por lo tanto, para el servicio child, podemos restringir su autowiring al tipo ChildClass:

services:
	parent: ParentClass
	child:
		create: ChildClass
		autowired: ChildClass   # también se puede escribir 'autowired: self'

	parentDep: ParentDependent  # autowiring pasa el servicio parent al constructor
	childDep: ChildDependent    # autowiring pasa el servicio child al constructor

Ahora, el servicio parent se pasa al constructor del servicio parentDep, porque ahora es el único objeto compatible. Autowiring ya no pasa el servicio child allí. Sí, el servicio child sigue siendo de tipo ParentClass, pero la condición de restricción dada para el tipo de parámetro ya no se cumple, es decir, no es cierto que ParentClass es un supertipo de ChildClass.

Para el servicio child, autowired: ChildClass también podría escribirse como autowired: self, ya que self es un marcador de posición para la clase del servicio actual.

En la clave autowired, también es posible especificar varias clases o interfaces como un array:

autowired: [BarClass, FooInterface]

Intentemos complementar el ejemplo con interfaces:

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 autowiring lo pasará allí.

Pero si restringimos su autowiring a ChildClass usando autowired: ChildClass (o self), autowiring solo lo pasará al constructor de ChildDependent, porque requiere un argumento de tipo ChildClass y es cierto que ChildClass es de tipo ChildClass. Ningún otro tipo especificado en los otros parámetros es un supertipo de ChildClass, por lo que el servicio no se pasa.

Si lo restringimos a ParentClass usando autowired: ParentClass, autowiring lo pasará nuevamente al constructor de ChildDependent (porque el ChildClass requerido es un supertipo de ParentClass) y ahora también al constructor de ParentDependent, porque el tipo requerido ParentClass también es compatible.

Si lo restringimos a FooInterface, seguirá siendo autowired en ParentDependent (el ParentClass requerido es un supertipo de FooInterface) y ChildDependent, pero además también en el constructor de FooDependent, pero no en BarDependent, porque BarInterface no es un supertipo de FooInterface.

services:
	child:
		create: ChildClass
		autowired: FooInterface

	fooDep: FooDependent        # autowiring pasa child al constructor
	barDep: BarDependent        # LANZARÁ EXCEPCIÓN, ningún servicio coincide
	parentDep: ParentDependent  # autowiring pasa child al constructor
	childDep: ChildDependent    # autowiring pasa child al constructor