Autowiring

L'autowiring è una funzionalità straordinaria che passa automaticamente al costruttore e agli altri metodi i servizi necessari, così non dobbiamo indicarli esplicitamente. Vi fa risparmiare molto tempo.

Grazie a esso 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 perché funzioni la classe ArticleRepository deve essere definita più o meno così:

namespace Model;

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

L'autowiring non usa mai i nomi dei servizi. Si basa esclusivamente sul sistema di tipi di PHP, quindi sa anche che una classe soddisfa le interfacce che implementa e le classi da cui eredita. Grazie a questo il nome di un servizio è solo un identificatore di comodo, e rinominarlo non rompe nulla nell'applicazione.

Perché si possa usare l'autowiring, nel container deve esserci esattamente un servizio di ogni tipo. Se ce ne fossero di più, l'autowiring non saprebbe quale passare e solleverebbe un'eccezione:

services:
	mainDb: PDO(%dsn%, %user%, %password%)
	tempDb: PDO('sqlite::memory:')
	articles: Model\ArticleRepository  # SOLLEVA UN'ECCEZIONE, corrispondono sia mainDb sia tempDb

Una soluzione è aggirare l'autowiring e indicare esplicitamente il nome del servizio (per esempio articles: Model\ArticleRepository(@mainDb)). Un approccio più comodo, però, è disattivare l'autowiring per uno dei servizi oppure preferire un servizio agli altri.

Disattivare l'autowiring

Possiamo disattivare l'autowiring di un servizio con l'opzione autowired: false:

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

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

	articles: Model\ArticleRepository  # al costruttore viene quindi passato mainDb

Il servizio articles non solleverà un'eccezione sulla presenza di due servizi PDO corrispondenti (mainDb e tempDb) disponibili per il costruttore, perché prende in considerazione solo il servizio mainDb.

L'autowiring si può disattivare globalmente anche per interi tipi, con l'opzione di configurazione di › excluded, che elenca i tipi (e i loro discendenti) che non devono mai subire l'autowiring.

La configurazione dell'autowiring in Nette differisce da quella di Symfony. In Symfony autowire: false significa che l'autowiring non deve essere usato per gli argomenti del costruttore del servizio. In Nette l'autowiring riguarda gli argomenti del costruttore e qualsiasi altro metodo richiamato tramite il container (come la setter injection). L'opzione autowired: false impedisce al container di passare automaticamente questa istanza di servizio come dipendenza ad altri servizi.

Preferenza nell'autowiring

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

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

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

	articles: Model\ArticleRepository

Il servizio articles non solleverà un'eccezione sulla presenza di più servizi PDO corrispondenti (mainDb e tempDb), ma userà quello preferito, cioè mainDb.

Collezione di servizi

L'autowiring può passare anche array di servizi di un determinato tipo. Poiché PHP non supporta nativamente l'indicazione del tipo degli elementi di un array nelle dichiarazioni di tipo, dovete integrare la dichiarazione array con un commento phpDoc che indichi il tipo degli elementi, come ClassName[]:

namespace Model;

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

Il container DI passa allora automaticamente un array dei servizi corrispondenti al tipo indicato. Omette i servizi che hanno l'autowiring disattivato e non include mai nella propria collezione il servizio attualmente in creazione. A differenza del passaggio di un singolo servizio, qui restringere l'autowiring a un tipo specifico o contrassegnare un servizio come preferito non ha alcun effetto: l'array contiene sempre tutti i servizi del tipo indicato.

Il tipo nel commento può avere anche la forma array<int, Class> oppure list<Class>. Se non potete controllare la forma del commento phpDoc, potete passare un array di servizi direttamente nella configurazione con typed().

Argomenti scalari

L'autowiring funziona solo per gli oggetti e per gli array di oggetti. Gli argomenti scalari (per esempio stringhe, numeri, booleani) vanno indicati nella configurazione. Un'alternativa è creare un oggetto di impostazioni che racchiuda il valore scalare (o più valori). Questo oggetto si può poi passare tramite autowiring.

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

Lo registrate come servizio aggiungendolo alla configurazione:

services:
	- MySettings('any value')

Le altre classi possono poi chiederlo tramite autowiring.

Dipendenze facoltative

Se un parametro del costruttore o di un metodo ha un valore predefinito e nel container non esiste alcun servizio del tipo richiesto, l'autowiring non solleva un'eccezione: salta semplicemente l'argomento, così viene usato il valore predefinito. È così che si dichiarano le dipendenze facoltative:

class Foo
{
	public function __construct(
		private ?Logger $logger = null,
	) {}
}

Al contrario, per un parametro senza valore predefinito un servizio mancante provoca sempre un'eccezione.

Restringere l'autowiring

Per i singoli servizi l'autowiring si può restringere a determinate classi o interfacce.

Normalmente l'autowiring passa un servizio a ogni parametro di metodo il cui tipo corrisponde al servizio. Restringere significa stabilire le condizioni che i tipi indicati per i parametri dei metodi devono soddisfare perché il servizio venga passato loro.

Prendiamo 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  # SOLLEVA UN'ECCEZIONE, corrispondono sia parent sia child
	childDep: ChildDependent    # l'autowiring passa al costruttore il servizio child

Il servizio parentDep solleva l'eccezione Multiple services of type ParentClass found: child, parent, perché nel suo costruttore entrano sia il servizio parent sia child e l'autowiring non riesce a decidere quale scegliere.

Per il servizio child possiamo quindi restringerne l'autowiring al tipo ChildClass:

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

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

Ora al costruttore del servizio parentDep viene passato il servizio parent, perché è ormai l'unico oggetto corrispondente. Il servizio child non gli viene più passato dall'autowiring. Sì, il servizio child è ancora di tipo ParentClass, ma la condizione restrittiva autowired: ChildClass fa sì che venga passato solo a parametri dichiarati esplicitamente come ChildClass (o come suoi sottotipi). Poiché ParentDependent richiede ParentClass, il servizio child non è più considerato lì un candidato per l'autowiring.

Per il servizio child, autowired: ChildClass si potrebbe scrivere anche come autowired: self, perché self è un segnaposto per la classe del servizio corrente.

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

autowired: [ParentClass, FooInterface]

Proviamo ad aggiungere delle interfacce all'esempio:

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, esso entrerà nei costruttori di tutte le classi FooDependent, BarDependent, ParentDependent e ChildDependent, e l'autowiring ve lo passerà.

Se però ne restringiamo l'autowiring a ChildClass con autowired: ChildClass (o self), l'autowiring lo passerà solo al costruttore di ChildDependent, perché richiede un argomento di tipo ChildClass e vale che ChildClass è di tipo ChildClass. Nessuno degli altri parametri richiede il tipo ChildClass o un suo sottotipo, quindi il servizio non viene passato loro.

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

Se lo limitiamo a FooInterface, subirà comunque l'autowiring in ParentDependent (il richiesto ParentClass è un sottotipo di FooInterface) e in ChildDependent, e in più nel costruttore di FooDependent, ma non in BarDependent, perché BarInterface non è un sottotipo di FooInterface.

services:
	child:
		create: ChildClass
		autowired: FooInterface

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