Autowiring

Το Autowiring είναι ένα εξαιρετικό χαρακτηριστικό που μπορεί να περάσει αυτόματα τις απαιτούμενες υπηρεσίες στον κατασκευαστή και σε άλλες μεθόδους, οπότε δεν χρειάζεται να τις γράψουμε καθόλου. Σας εξοικονομεί πολύ χρόνο.

Χάρη σε αυτό, μπορούμε να παραλείψουμε τη συντριπτική πλειοψηφία των ορισμάτων κατά τη σύνταξη ορισμών υπηρεσιών. Αντί για:

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

Αρκεί να γράψουμε:

services:
	articles: Model\ArticleRepository

Το Autowiring καθοδηγείται από τους τύπους, οπότε για να λειτουργήσει, η κλάση ArticleRepository πρέπει να οριστεί κάπως έτσι:

namespace Model;

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

Για να είναι δυνατή η χρήση του autowiring, πρέπει να υπάρχει ακριβώς μία υπηρεσία για κάθε τύπο στο container. Αν υπήρχαν περισσότερες, το autowiring δεν θα ήξερε ποια να περάσει και θα προκαλούσε εξαίρεση:

services:
	mainDb: PDO(%dsn%, %user%, %password%)
	tempDb: PDO('sqlite::memory:')
	articles: Model\ArticleRepository  # ΠΡΟΚΑΛΕΙ ΕΞΑΙΡΕΣΗ, ταιριάζουν και η mainDb και η tempDb

Η λύση θα ήταν είτε να παρακάμψουμε το autowiring και να δηλώσουμε ρητά το όνομα της υπηρεσίας (δηλ. articles: Model\ArticleRepository(@mainDb)). Πιο έξυπνο όμως είναι να απενεργοποιήσουμε το autowiring για μία από τις υπηρεσίες, ή να δώσουμε προτεραιότητα στην πρώτη υπηρεσία.

Απενεργοποίηση του autowiring

Μπορούμε να απενεργοποιήσουμε το autowiring μιας υπηρεσίας χρησιμοποιώντας την επιλογή autowired: no:

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

	tempDb:
		create: PDO('sqlite::memory:')
		autowired: false               # η υπηρεσία tempDb εξαιρείται από το autowiring

	articles: Model\ArticleRepository  # επομένως περνάει τη mainDb στον κατασκευαστή

Η υπηρεσία articles δεν προκαλεί εξαίρεση ότι υπάρχουν δύο κατάλληλες υπηρεσίες τύπου PDO (δηλ. mainDb και tempDb) που μπορούν να περάσουν στον κατασκευαστή, επειδή βλέπει μόνο την υπηρεσία mainDb.

Η διαμόρφωση του autowiring στο Nette λειτουργεί διαφορετικά από ό,τι στο Symfony, όπου η επιλογή autowire: false λέει ότι το autowiring δεν πρέπει να χρησιμοποιείται για τα ορίσματα του κατασκευαστή της συγκεκριμένης υπηρεσίας. Στο Nette, το autowiring χρησιμοποιείται πάντα, είτε για τα ορίσματα του κατασκευαστή, είτε για οποιαδήποτε άλλη μέθοδο. Η επιλογή autowired: false λέει ότι η παρουσία της συγκεκριμένης υπηρεσίας δεν πρέπει να περνιέται πουθενά μέσω autowiring.

Προτίμηση autowiring

Εάν έχουμε πολλές υπηρεσίες του ίδιου τύπου και σε μία από αυτές δηλώσουμε την επιλογή autowired, αυτή η υπηρεσία γίνεται η προτιμώμενη:

services:
	mainDb:
		create: PDO(%dsn%, %user%, %password%)
		autowired: PDO    # γίνεται η προτιμώμενη

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

	articles: Model\ArticleRepository

Η υπηρεσία articles δεν προκαλεί εξαίρεση ότι υπάρχουν δύο κατάλληλες υπηρεσίες τύπου PDO (δηλ. mainDb και tempDb), αλλά χρησιμοποιεί την προτιμώμενη υπηρεσία, δηλαδή τη mainDb.

Πίνακας υπηρεσιών

Το Autowiring μπορεί επίσης να περάσει πίνακες υπηρεσιών ενός συγκεκριμένου τύπου. Επειδή στην PHP δεν είναι δυνατό να γραφτεί εγγενώς ο τύπος των στοιχείων του πίνακα, είναι απαραίτητο, εκτός από τον τύπο array, να συμπληρωθεί και ένα phpDoc σχόλιο με τον τύπο του στοιχείου στη μορφή ClassName[]:

namespace Model;

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

Το DI container στη συνέχεια περνά αυτόματα έναν πίνακα υπηρεσιών που αντιστοιχούν στον συγκεκριμένο τύπο. Παραλείπει τις υπηρεσίες που έχουν απενεργοποιημένο το autowiring.

Ο τύπος στο σχόλιο μπορεί επίσης να είναι στη μορφή array<int, Class> ή list<Class>. Εάν δεν μπορείτε να επηρεάσετε τη μορφή του phpDoc σχολίου, μπορείτε να περάσετε τον πίνακα υπηρεσιών απευθείας στη διαμόρφωση χρησιμοποιώντας το typed().

Σκαλωτά ορίσματα

Το Autowiring μπορεί να αντικαταστήσει μόνο αντικείμενα και πίνακες αντικειμένων. Τα σκαλωτά ορίσματα (π.χ. συμβολοσειρές, αριθμοί, booleans) τα γράφουμε στη διαμόρφωση. Μια εναλλακτική λύση είναι να δημιουργήσετε ένα settings-object, το οποίο ενσωματώνει την σκαλωτή τιμή (ή περισσότερες τιμές) σε μορφή αντικειμένου, το οποίο στη συνέχεια μπορεί να περάσει ξανά μέσω autowiring.

class MySettings
{
	public function __construct(
		// το readonly είναι δυνατό να χρησιμοποιηθεί από την PHP 8.1
		public readonly bool $value,
	)
	{}
}

Δημιουργείτε μια υπηρεσία από αυτό προσθέτοντάς το στη διαμόρφωση:

services:
	- MySettings('any value')

Όλες οι κλάσεις στη συνέχεια το ζητούν μέσω autowiring.

Περιορισμός του autowiring

Για μεμονωμένες υπηρεσίες, το autowiring μπορεί να περιοριστεί μόνο σε συγκεκριμένες κλάσεις ή interfaces.

Κανονικά, το autowiring περνά την υπηρεσία σε κάθε παράμετρο μεθόδου, ο τύπος της οποίας αντιστοιχεί στην υπηρεσία. Ο περιορισμός σημαίνει ότι θέτουμε συνθήκες που πρέπει να πληρούν οι τύποι που αναφέρονται στις παραμέτρους των μεθόδων, ώστε η υπηρεσία να τους περάσει.

Ας το δείξουμε με ένα παράδειγμα:

class ParentClass
{}

class ChildClass extends ParentClass
{}

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

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

Αν τις καταχωρούσαμε όλες ως υπηρεσίες, το autowiring θα αποτύγχανε:

services:
	parent: ParentClass
	child: ChildClass
	parentDep: ParentDependent  # ΠΡΟΚΑΛΕΙ ΕΞΑΙΡΕΣΗ, ταιριάζουν οι υπηρεσίες parent και child
	childDep: ChildDependent    # το autowiring περνά την υπηρεσία child στον κατασκευαστή

Η υπηρεσία parentDep προκαλεί εξαίρεση Multiple services of type ParentClass found: parent, child, επειδή στον κατασκευαστή της ταιριάζουν και οι δύο υπηρεσίες parent και child, και το autowiring δεν μπορεί να αποφασίσει ποια να επιλέξει.

Για την υπηρεσία child, μπορούμε επομένως να περιορίσουμε το autowiring της στον τύπο ChildClass:

services:
	parent: ParentClass
	child:
		create: ChildClass
		autowired: ChildClass   # μπορεί να γραφτεί και 'autowired: self'

	parentDep: ParentDependent  # το autowiring περνά την υπηρεσία parent στον κατασκευαστή
	childDep: ChildDependent    # το autowiring περνά την υπηρεσία child στον κατασκευαστή

Τώρα, στον κατασκευαστή της υπηρεσίας parentDep περνιέται η υπηρεσία parent, επειδή τώρα είναι το μόνο κατάλληλο αντικείμενο. Το autowiring δεν περνά πλέον την υπηρεσία child εκεί. Ναι, η υπηρεσία child εξακολουθεί να είναι τύπου ParentClass, αλλά η περιοριστική συνθήκη που δόθηκε για τον τύπο της παραμέτρου δεν ισχύει πλέον, δηλ. δεν ισχύει ότι το ParentClass είναι υπερτύπος του ChildClass.

Για την υπηρεσία child, το autowired: ChildClass θα μπορούσε επίσης να γραφτεί ως autowired: self, καθώς το self είναι ένα placeholder για την κλάση της τρέχουσας υπηρεσίας.

Στο κλειδί autowired, είναι δυνατόν να αναφερθούν και πολλές κλάσεις ή interfaces ως πίνακας:

autowired: [BarClass, FooInterface]

Ας δοκιμάσουμε να συμπληρώσουμε το παράδειγμα και με 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)
	{}
}

Όταν η υπηρεσία child δεν περιορίζεται καθόλου, θα ταιριάζει στους κατασκευαστές όλων των κλάσεων FooDependent, BarDependent, ParentDependent και ChildDependent και το autowiring θα την περάσει εκεί.

Αν όμως περιορίσουμε το autowiring της σε ChildClass χρησιμοποιώντας autowired: ChildClassself), το autowiring θα την περάσει μόνο στον κατασκευαστή του ChildDependent, επειδή απαιτεί όρισμα τύπου ChildClass και ισχύει ότι το ChildClass είναι τύπου ChildClass. Κανένας άλλος τύπος που αναφέρεται στις άλλες παραμέτρους δεν είναι υπερτύπος του ChildClass, οπότε η υπηρεσία δεν περνιέται.

Αν το περιορίσουμε σε ParentClass χρησιμοποιώντας autowired: ParentClass, το autowiring θα την περάσει ξανά στον κατασκευαστή του ChildDependent (επειδή το απαιτούμενο ChildClass είναι υπερτύπος του ParentClass) και τώρα και στον κατασκευαστή του ParentDependent, επειδή ο απαιτούμενος τύπος ParentClass είναι επίσης κατάλληλος.

Αν το περιορίσουμε σε FooInterface, θα εξακολουθεί να γίνεται autowired στο ParentDependent (το απαιτούμενο ParentClass είναι υπερτύπος του FooInterface) και στο ChildDependent, αλλά επιπλέον και στον κατασκευαστή του FooDependent, όχι όμως στο BarDependent, επειδή το BarInterface δεν είναι υπερτύπος του FooInterface.

services:
	child:
		create: ChildClass
		autowired: FooInterface

	fooDep: FooDependent        # το autowiring περνά το child στον κατασκευαστή
	barDep: BarDependent        # ΠΡΟΚΑΛΕΙ ΕΞΑΙΡΕΣΗ, καμία υπηρεσία δεν ταιριάζει
	parentDep: ParentDependent  # το autowiring περνά το child στον κατασκευαστή
	childDep: ChildDependent    # το autowiring περνά το child στον κατασκευαστή
έκδοση: 3.x