Autowiring

Autowiring é um ótimo recurso que pode passar automaticamente os serviços necessários para o construtor e outros métodos, para que não precisemos escrevê-los. Isso economiza muito tempo.

Graças a isso, podemos omitir a grande maioria dos argumentos ao escrever definições de serviço. Em vez de:

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

Basta escrever:

services:
	articles: Model\ArticleRepository

O Autowiring é orientado por tipos, então para funcionar, a classe ArticleRepository deve ser definida aproximadamente assim:

namespace Model;

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

Para poder usar o autowiring, deve haver exatamente um serviço para cada tipo no contêiner. Se houver mais, o autowiring não saberá qual passar e lançará uma exceção:

services:
	mainDb: PDO(%dsn%, %user%, %password%)
	tempDb: PDO('sqlite::memory:')
	articles: Model\ArticleRepository  # LANÇARÁ EXCEÇÃO, tanto mainDb quanto tempDb correspondem

A solução seria contornar o autowiring e especificar explicitamente o nome do serviço (ou seja, articles: Model\ArticleRepository(@mainDb)). Mas é mais inteligente desativar o autowiring para um dos serviços, ou dar preferência ao primeiro serviço.

Desativação do autowiring

Podemos desativar o autowiring de um serviço usando a opção autowired: no:

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

	tempDb:
		create: PDO('sqlite::memory:')
		autowired: false               # o serviço tempDb é excluído do autowiring

	articles: Model\ArticleRepository  # portanto, passa mainDb para o construtor

O serviço articles não lançará uma exceção dizendo que existem dois serviços do tipo PDO correspondentes (ou seja, mainDb e tempDb) que podem ser passados para o construtor, porque ele vê apenas o serviço mainDb.

A configuração do autowiring no Nette funciona de forma diferente do Symfony, onde a opção autowire: false diz que o autowiring não deve ser usado para os argumentos do construtor do serviço fornecido. No Nette, o autowiring é sempre usado, seja para argumentos do construtor ou para quaisquer outros métodos. A opção autowired: false diz que a instância do serviço fornecido não deve ser passada para lugar nenhum usando autowiring.

Preferência de autowiring

Se tivermos vários serviços do mesmo tipo e especificarmos a opção autowired para um deles, esse serviço se torna o preferido:

services:
	mainDb:
		create: PDO(%dsn%, %user%, %password%)
		autowired: PDO    # torna-se preferido

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

	articles: Model\ArticleRepository

O serviço articles não lançará uma exceção dizendo que existem dois serviços do tipo PDO correspondentes (ou seja, mainDb e tempDb), mas usará o serviço preferido, ou seja, mainDb.

Array de serviços

O Autowiring também pode passar arrays de serviços de um determinado tipo. Como não é possível escrever nativamente o tipo dos itens do array em PHP, é necessário, além do tipo array, adicionar um comentário phpDoc com o tipo do item no formato ClassName[]:

namespace Model;

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

O contêiner DI então passa automaticamente um array de serviços correspondentes ao tipo fornecido. Ele omite serviços que têm o autowiring desativado.

O tipo no comentário também pode estar no formato array<int, Class> ou list<Class>. Se você não pode influenciar a forma do comentário phpDoc, pode passar o array de serviços diretamente na configuração usando typed().

Argumentos escalares

O Autowiring só pode injetar objetos e arrays de objetos. Argumentos escalares (por exemplo, strings, números, booleanos) são escritos na configuração. Uma alternativa é criar um objeto de configurações, que encapsula o valor escalar (ou múltiplos valores) em um objeto, que pode então ser passado novamente usando autowiring.

class MySettings
{
	public function __construct(
		// readonly pode ser usado a partir do PHP 8.1
		public readonly bool $value,
	)
	{}
}

Você cria um serviço a partir dele adicionando-o à configuração:

services:
	- MySettings('any value')

Todas as classes então o solicitarão usando autowiring.

Restringindo o autowiring

Para serviços individuais, o autowiring pode ser restrito a certas classes ou interfaces.

Normalmente, o autowiring passa o serviço para cada parâmetro de método cujo tipo o serviço corresponde. Restringir significa que estabelecemos condições que os tipos especificados nos parâmetros do método devem satisfazer para que o serviço seja passado para eles.

Vamos ilustrar com um exemplo:

class ParentClass
{}

class ChildClass extends ParentClass
{}

class ParentDependent
{
	function __construct(ParentClass $obj)
	{}
}

class ChildDependent
{
	function __construct(ChildClass $obj)
	{}
}

Se registrássemos todos eles como serviços, o autowiring falharia:

services:
	parent: ParentClass
	child: ChildClass
	parentDep: ParentDependent  # LANÇARÁ EXCEÇÃO, os serviços parent e child correspondem
	childDep: ChildDependent    # autowiring passa o serviço child para o construtor

O serviço parentDep lançará a exceção Multiple services of type ParentClass found: parent, child, porque ambos os serviços parent e child se encaixam em seu construtor, e o autowiring não pode decidir qual escolher.

Para o serviço child, podemos, portanto, restringir seu autowiring ao tipo ChildClass:

services:
	parent: ParentClass
	child:
		create: ChildClass
		autowired: ChildClass   # também pode escrever 'autowired: self'

	parentDep: ParentDependent  # autowiring passa o serviço parent para o construtor
	childDep: ChildDependent    # autowiring passa o serviço child para o construtor

Agora, o serviço parent é passado para o construtor do serviço parentDep, porque agora é o único objeto correspondente. O autowiring não passa mais o serviço child para lá. Sim, o serviço child ainda é do tipo ParentClass, mas a condição restritiva dada para o tipo do parâmetro não é mais válida, ou seja, não é verdade que ParentClass é um supertipo de ChildClass.

Para o serviço child, autowired: ChildClass também poderia ser escrito como autowired: self, já que self é um placeholder para a classe do serviço atual.

Na chave autowired, também é possível especificar várias classes ou interfaces como um array:

autowired: [BarClass, FooInterface]

Vamos tentar complementar o exemplo com 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)
	{}
}

Se não restringirmos o serviço child de forma alguma, ele se encaixará nos construtores de todas as classes FooDependent, BarDependent, ParentDependent e ChildDependent, e o autowiring o passará para lá.

No entanto, se restringirmos seu autowiring a ChildClass usando autowired: ChildClass (ou self), o autowiring o passará apenas para o construtor de ChildDependent, porque ele requer um argumento do tipo ChildClass e é verdade que ChildClass é do tipo ChildClass. Nenhum outro tipo especificado nos outros parâmetros é um supertipo de ChildClass, então o serviço não é passado.

Se o restringirmos a ParentClass usando autowired: ParentClass, ele será novamente passado para o construtor de ChildDependent (porque o ChildClass exigido é um supertipo de ParentClass) e, agora também para o construtor de ParentDependent, porque o tipo ParentClass exigido também é adequado.

Se o restringirmos a FooInterface, ele ainda será autowired para ParentDependent (o ParentClass exigido é um supertipo de FooInterface) e ChildDependent, mas adicionalmente também para o construtor de FooDependent, mas não para BarDependent, porque BarInterface não é um supertipo de FooInterface.

services:
	child:
		create: ChildClass
		autowired: FooInterface

	fooDep: FooDependent        # autowiring passa child para o construtor
	barDep: BarDependent        # LANÇARÁ EXCEÇÃO, nenhum serviço corresponde
	parentDep: ParentDependent  # autowiring passa child para o construtor
	childDep: ChildDependent    # autowiring passa child para o construtor