Autowiring

L'Autowiring è una funzionalità fantastica che può passare automaticamente i servizi richiesti al costruttore e ad altri metodi, quindi non dobbiamo scriverli affatto. Ti fa risparmiare un sacco di tempo.

Grazie a questo, possiamo omettere la stragrande maggioranza degli argomenti quando scriviamo le definizioni dei servizi. Invece di:

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

Basta scrivere:

services:
	articles: Model\ArticleRepository

L'Autowiring si basa sui tipi, quindi affinché funzioni, la classe ArticleRepository deve essere definita più o meno così:

namespace Model;

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

Per poter utilizzare l'autowiring, deve esserci esattamente un servizio per ogni tipo nel container. Se ce ne fossero di più, l'autowiring non saprebbe quale passare e lancerebbe un'eccezione:

services:
	mainDb: PDO(%dsn%, %user%, %password%)
	tempDb: PDO('sqlite::memory:')
	articles: Model\ArticleRepository  # LANCIA ECCEZIONE, soddisfano sia mainDb che tempDb

La soluzione sarebbe bypassare l'autowiring e specificare esplicitamente il nome del servizio (cioè articles: Model\ArticleRepository(@mainDb)). Ma è più intelligente disattivare l'autowiring per uno dei servizi, o dare la preferenza al primo servizio.

Disattivazione dell'autowiring

Possiamo disattivare l'autowiring di un servizio usando l'opzione autowired: no:

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

	tempDb:
		create: PDO('sqlite::memory:')
		autowired: false               # il servizio tempDb è escluso dall'autowiring

	articles: Model\ArticleRepository  # quindi passa mainDb al costruttore

Il servizio articles non lancerà un'eccezione perché esistono due servizi compatibili di tipo PDO (cioè mainDb e tempDb) che possono essere passati al costruttore, perché vede solo il servizio mainDb.

La configurazione dell'autowiring in Nette funziona diversamente rispetto a Symfony, dove l'opzione autowire: false indica che l'autowiring non deve essere utilizzato per gli argomenti del costruttore del servizio specificato. In Nette, l'autowiring viene sempre utilizzato, sia per gli argomenti del costruttore che per qualsiasi altro metodo. L'opzione autowired: false indica che l'istanza del servizio specificato non deve essere passata da nessuna parte tramite autowiring.

Preferenza dell'autowiring

Se abbiamo più servizi dello stesso tipo e per uno di essi specifichiamo l'opzione autowired, questo servizio diventa preferito:

services:
	mainDb:
		create: PDO(%dsn%, %user%, %password%)
		autowired: PDO    # diventa preferito

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

	articles: Model\ArticleRepository

Il servizio articles non lancerà un'eccezione perché esistono due servizi compatibili di tipo PDO (cioè mainDb e tempDb), ma utilizzerà il servizio preferito, ovvero mainDb.

Array di servizi

L'Autowiring può anche passare array di servizi di un certo tipo. Poiché in PHP non è possibile scrivere nativamente il tipo degli elementi dell'array, è necessario aggiungere, oltre al tipo array, anche un commento phpDoc con il tipo dell'elemento nella forma ClassName[]:

namespace Model;

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

Il container DI passerà quindi automaticamente un array di servizi corrispondenti al tipo specificato. Ometterà i servizi che hanno l'autowiring disattivato.

Il tipo nel commento può anche essere nella forma array<int, Class> o list<Class>. Se non puoi influenzare la forma del commento phpDoc, puoi passare l'array di servizi direttamente nella configurazione usando typed().

Argomenti scalari

L'Autowiring può fornire solo oggetti e array di oggetti. Gli argomenti scalari (ad es. stringhe, numeri, booleani) li scriviamo nella configurazione. Un'alternativa è creare un oggetto-impostazioni, che incapsula il valore scalare (o più valori) in un oggetto, che può poi essere nuovamente passato tramite autowiring.

class MySettings
{
	public function __construct(
		// readonly può essere usato da PHP 8.1
		public readonly bool $value,
	)
	{}
}

Lo trasformi in un servizio aggiungendolo alla configurazione:

services:
	- MySettings('any value')

Tutte le classi lo richiederanno quindi tramite autowiring.

Restrizione dell'autowiring

Per singoli servizi, l'autowiring può essere ristretto a determinate classi o interfacce.

Normalmente, l'autowiring passa un servizio a ogni parametro di metodo il cui tipo corrisponde al servizio. La restrizione significa che stabiliamo condizioni che i tipi specificati nei parametri dei metodi devono soddisfare affinché il servizio venga loro passato.

Lo mostreremo con un esempio:

class ParentClass
{}

class ChildClass extends ParentClass
{}

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

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

Se li registrassimo tutti come servizi, l'autowiring fallirebbe:

services:
	parent: ParentClass
	child: ChildClass
	parentDep: ParentDependent  # LANCIA ECCEZIONE, soddisfano i servizi parent e child
	childDep: ChildDependent    # l'autowiring passa il servizio child al costruttore

Il servizio parentDep lancerà l'eccezione Multiple services of type ParentClass found: parent, child, perché entrambi i servizi parent e child corrispondono al suo costruttore, e l'autowiring non può decidere quale scegliere.

Per il servizio child, possiamo quindi restringere il suo autowiring al tipo ChildClass:

services:
	parent: ParentClass
	child:
		create: ChildClass
		autowired: ChildClass   # si può anche scrivere 'autowired: self'

	parentDep: ParentDependent  # l'autowiring passa il servizio parent al costruttore
	childDep: ChildDependent    # l'autowiring passa il servizio child al costruttore

Ora, al costruttore del servizio parentDep viene passato il servizio parent, perché ora è l'unico oggetto compatibile. L'autowiring non passerà più il servizio child lì. Sì, il servizio child è ancora di tipo ParentClass, ma la condizione restrittiva data per il tipo del parametro non è più valida, cioè non è vero che ParentClass è un supertipo di ChildClass.

Per il servizio child, autowired: ChildClass potrebbe anche essere scritto come autowired: self, poiché self è un segnaposto per la classe del servizio corrente.

Nella chiave autowired è possibile specificare anche più classi o interfacce come array:

autowired: [BarClass, FooInterface]

Proviamo a completare l'esempio con un'interfaccia:

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 non limitiamo in alcun modo il servizio child, corrisponderà ai costruttori di tutte le classi FooDependent, BarDependent, ParentDependent e ChildDependent e l'autowiring lo passerà lì.

Ma se restringiamo il suo autowiring a ChildClass usando autowired: ChildClass (o self), l'autowiring lo passerà solo al costruttore di ChildDependent, perché richiede un argomento di tipo ChildClass ed è vero che ChildClass è di tipo ChildClass. Nessun altro tipo specificato negli altri parametri è un supertipo di ChildClass, quindi il servizio non viene passato.

Se lo limitiamo a ParentClass usando autowired: ParentClass, l'autowiring lo passerà di nuovo al costruttore di ChildDependent (perché il ChildClass richiesto è un supertipo di ParentClass) e ora anche al costruttore di ParentDependent, perché anche il tipo ParentClass richiesto è compatibile.

Se lo limitiamo a FooInterface, sarà ancora autowired in ParentDependent (il ParentClass richiesto è un supertipo di FooInterface) e ChildDependent, ma inoltre anche nel costruttore di FooDependent, ma non in BarDependent, perché BarInterface non è un supertipo di FooInterface.

services:
	child:
		create: ChildClass
		autowired: FooInterface

	fooDep: FooDependent        # l'autowiring passa child al costruttore
	barDep: BarDependent        # LANCIA ECCEZIONE, nessun servizio corrisponde
	parentDep: ParentDependent  # l'autowiring passa child al costruttore
	childDep: ChildDependent    # l'autowiring passa child al costruttore
versione: 3.x