Drupal 8 Iniezione corretta delle dipendenze con DI

Siccome sono sicuro che ormai l'integrazione di dipendenza (DI) e il contenitore di servizi Symfony sono importanti nuove funzionalità di sviluppo di Drupal 8. Tuttavia, anche se stanno iniziando a essere meglio compresi nella comunità di sviluppo di Drupal, c'è ancora un po 'di carenza. di chiarezza su come esattamente iniettare servizi nelle classi Drupal 8.

Molti esempi parlano di servizi, ma la maggior parte riguarda solo il modo statico di caricarli:

$ service = \ Drupal :: service ('nome_servizio');

Ciò è comprensibile in quanto l'approccio di iniezione corretto è più dettagliato e, se lo si conosce già, è piuttosto semplice. Tuttavia, l'approccio statico in vita reale dovrebbe essere usato solo in due casi:

  • nel .modulo file (al di fuori di un contesto di classe)
  • quelle rare occasioni all'interno di un contesto di classe in cui la classe viene caricata senza consapevolezza del contenitore di servizi

Oltre a questo, i servizi di iniezione sono la migliore pratica in quanto garantisce il codice disaccoppiato e facilita i test.

In Drupal 8 ci sono alcune specificità sull'iniezione di dipendenza che non sarete in grado di comprendere esclusivamente da un approccio di Symfony puro. Quindi in questo articolo vedremo alcuni esempi di corretta iniezione del costruttore in Drupal 8. A tal fine, ma anche per coprire tutte le basi, vedremo tre tipi di esempi, in ordine di complessità:

  • iniettare servizi in un altro dei propri servizi
  • iniettare servizi in classi non di servizio
  • iniettare servizi nelle classi di plugin

Andando avanti, il presupposto è che tu sai già cosa è DI, quale scopo serve e come il contenitore di servizi lo supporta. In caso contrario, consiglio di consultare prima questo articolo.

Servizi

L'iniezione di servizi nel proprio servizio è molto semplice. Dato che sei tu a definire il servizio, tutto ciò che devi fare è passarlo come argomento al servizio che vuoi iniettare. Immagina le seguenti definizioni di servizio:

servizi: demo.demo_service: class: Drupal \ demo \ DemoService demo.another_demo_service: class: Drupal \ demo \ Argomenti AnotherDemoService: ['@ demo.demo_service']

Qui definiamo due servizi in cui il secondo accetta il primo come argomento del costruttore. Quindi tutto ciò che dobbiamo fare ora nel AnotherDemoService la classe è memorizzarla come variabile locale:

class AnotherDemoService / ** * @var \ Drupal \ demo \ DemoService * / private $ demoService; funzione pubblica __construct (DemoService $ demoService) $ this-> demoService = $ demoService;  // Il resto dei tuoi metodi 

E questo è praticamente tutto. È anche importante ricordare che questo approccio è esattamente lo stesso di Symfony, quindi non cambiare qui.

Classi non di servizio

Ora diamo un'occhiata alle classi con cui spesso interagiamo ma che non sono i nostri servizi. Per capire come avviene questa iniezione, è necessario capire come vengono risolte le classi e come vengono istanziate. Ma lo vedremo presto in pratica.

Controller

Le classi controller sono utilizzate principalmente per mappare i percorsi di routing alla logica aziendale. Si suppone che restino magri e delegano ai servizi servizi logistici più pesanti. Molti estendono il ControllerBase classe e ottenere alcuni metodi di supporto per recuperare servizi comuni dal contenitore. Tuttavia, questi vengono restituiti staticamente.

Quando viene creato un oggetto controller (ControllerResolver :: createController), il ClassResolver viene utilizzato per ottenere un'istanza della definizione della classe controller. Il resolver è consapevole del contenitore e restituisce un'istanza del controller se il contenitore lo ha già. Al contrario, crea un'istanza nuova e la restituisce. 

Ed ecco dove avviene la nostra iniezione: se la classe che viene risolta implementa il ContainerAwareInterface, l'istanziazione avviene usando la statica creare() metodo su quella classe che riceve l'intero contenitore. E il nostro ControllerBase la classe implementa anche il ContainerAwareInterface.

Diamo un'occhiata a un controller di esempio che inietta correttamente i servizi utilizzando questo approccio (invece di richiederli staticamente):

/ ** * Definisce un controller per elencare i blocchi. * / class BlockListController estende EntityListController / ** * Il gestore di temi. * * @var \ Drupal \ Core \ Extension \ ThemeHandlerInterface * / protected $ themeHandler; / ** * Costruisce BlockListController. * * @param \ Drupal \ Core \ Extension \ ThemeHandlerInterface $ theme_handler * Il gestore di temi. * / public function __construct (ThemeHandlerInterface $ theme_handler) $ this-> themeHandler = $ theme_handler;  / ** * @inheritdoc * / public static function create (ContainerInterface $ container) return new static ($ container-> get ('theme_handler')); 

Il EntityListController la classe non fa nulla per i nostri scopi qui, quindi immagina BlockListController estende direttamente il ControllerBase classe, che a sua volta implementa il ContainerInjectionInterface.

Come abbiamo detto, quando questo controller viene istanziato, lo statico creare() il metodo è chiamato. Il suo scopo è di creare un'istanza di questa classe e passare i parametri che vuole al costruttore della classe. E dal momento che il contenitore è passato a creare(), può scegliere quali servizi richiedere e passare al costruttore. 

Quindi, il costruttore deve semplicemente ricevere i servizi e memorizzarli localmente. Tieni presente che è una cattiva pratica iniettare l'intero contenitore nella tua classe e dovresti sempre limitare i servizi che inietti a quelli di cui hai bisogno. E se ne hai bisogno di troppi, probabilmente stai facendo qualcosa di sbagliato.

Abbiamo utilizzato questo esempio di controller per approfondire l'approccio dell'iniezione di dipendenza da Drupal e capire come funziona l'iniezione del costruttore. Ci sono anche possibilità di iniezione setter rendendo consapevoli le classi container, ma non lo riguarderemo qui. Vediamo invece altri esempi di classi con cui è possibile interagire e in cui è necessario iniettare servizi.

Le forme

I moduli sono un altro grande esempio di classi in cui è necessario iniettare servizi. Di solito o estendi il FormBase o ConfigFormBase classi che già implementano il ContainerInjectionInterface. In questo caso, se si sostituisce il creare() e metodi di costruzione, puoi iniettare tutto ciò che vuoi. Se non vuoi estendere queste classi, tutto ciò che devi fare è implementare questa interfaccia da solo e seguire gli stessi passi che abbiamo visto sopra con il controller.

Ad esempio, diamo un'occhiata al SiteInformationForm che estende il ConfigFormBase e vedere come inietta i servizi in cima al config.factory il suo genitore ha bisogno di:

class SiteInformationForm estende ConfigFormBase ... public function __construct (ConfigFactoryInterface $ config_factory, AliasManagerInterface $ alias_manager, PathValidatorInterface $ path_validator, RequestContext $ request_context) parent :: __ construct ($ config_factory); $ this-> aliasManager = $ alias_manager; $ this-> pathValidator = $ path_validator; $ this-> requestContext = $ request_context;  / ** * @inheritdoc * / public static function create (ContainerInterface $ container) return new static ($ container-> get ('config.factory'), $ container-> get ('path.alias_manager') , $ container-> get ('path.validator'), $ container-> get ('router.request_context'));  ...

Come prima, il creare() il metodo è usato per l'istanziazione, che passa al costruttore il servizio richiesto dalla classe genitore e alcuni extra di cui ha bisogno in cima.

E questo è più o meno come funziona l'iniezione di base del costruttore in Drupal 8. È disponibile in quasi tutti i contesti di classe, ad eccezione di alcuni in cui la parte di istanziazione non è stata ancora risolta in questo modo (ad esempio i plugin FieldType). Inoltre, esiste un sottosistema importante che presenta alcune differenze, ma è di fondamentale importanza comprendere: i plugin.

plugin

Il sistema di plugin è un componente Drupal 8 molto importante che alimenta molte funzionalità. Quindi vediamo come funziona la dipendenza con le classi del plugin.

La differenza più importante nel modo in cui l'iniezione viene gestita con i plugin è che le classi del plug-in di interfaccia devono implementare: ContainerFactoryPluginInterface. Il motivo è che i plugin non sono risolti ma sono gestiti da un gestore di plugin. Quindi, quando questo manager ha bisogno di istanziare uno dei suoi plugin, lo farà usando una fabbrica. E di solito, questa fabbrica è la ContainerFactory (o una variazione simile di esso). 

Quindi se guardiamo ContainerFactory :: createInstance (), vediamo che a parte il contenitore viene passato al solito creare() metodo, il configurazione $, $ plugin_id, e $ plugin_definition anche le variabili vengono passate (quali sono i tre parametri di base di ciascun plugin).

Quindi vediamo due esempi di tali plugin che iniettano servizi. In primo luogo, il nucleo UserLoginBlock collegare (@Bloccare):

classe UserLoginBlock estende BlockBase implementa ContainerFactoryPluginInterface ... public function __construct (array $ configuration, $ plugin_id, $ plugin_definition, RouteMatchInterface $ route_match) parent :: __ construct ($ configuration, $ plugin_id, $ plugin_definition); $ this-> routeMatch = $ route_match;  / ** * @inheritdoc * / public static function create (ContainerInterface $ container, array $ configuration, $ plugin_id, $ plugin_definition) return new static ($ configuration, $ plugin_id, $ plugin_definition, $ container-> get ( 'current_route_match'));  ...

Come puoi vedere, implementa il ContainerFactoryPluginInterface e il creare() il metodo riceve questi tre parametri aggiuntivi. Questi vengono quindi passati nell'ordine corretto al costruttore della classe e dal contenitore viene richiesto e passato anche un servizio. Questo è l'esempio più basilare, ma comunemente usato, di servizi di iniezione nelle classi di plugin.

Un altro esempio interessante è il FileWidget collegare (@FieldWidget):

classe FileWidget estende WidgetBase implementa ContainerFactoryPluginInterface / ** * @inheritdoc * / public function __construct ($ plugin_id, $ plugin_definition, FieldDefinitionInterface $ field_definition, array $ settings, array $ third_party_settings, ElementInfoManagerInterface $ element_info) parent :: __ construct ($ plugin_id, $ plugin_definition, $ field_definition, $ settings, $ third_party_settings); $ this-> elementInfo = $ element_info;  / ** * @inheritdoc * / public static function create (ContainerInterface $ container, array $ configuration, $ plugin_id, $ plugin_definition) return new static ($ plugin_id, $ plugin_definition, $ configuration ['field_definition'], $ configurazione ['settings'], $ configuration ['third_party_settings'], $ container-> get ('element_info'));  ...

Come puoi vedere, il creare() il metodo riceve gli stessi parametri, ma il costruttore della classe si aspetta quelli aggiuntivi specifici per questo tipo di plugin. Questo non è un problema. Di solito possono essere trovati all'interno del configurazione $ array di quel particolare plugin e passato da lì.

Quindi queste sono le principali differenze quando si tratta di iniettare servizi nelle classi di plugin. C'è un'interfaccia diversa da implementare e alcuni parametri extra in creare() metodo.

Conclusione

Come abbiamo visto in questo articolo, ci sono diversi modi in cui possiamo mettere le mani su servizi in Drupal 8. A volte dobbiamo richiederli staticamente. Tuttavia, la maggior parte delle volte non dovremmo. E abbiamo visto alcuni tipici esempi di quando e come dovremmo iniettarli nelle nostre classi. Abbiamo anche visto le due interfacce principali che le classi devono implementare per essere istanziate con il contenitore ed essere pronte per l'iniezione, così come la differenza tra loro.

Se stai lavorando in un contesto di classe e non sei sicuro di come iniettare servizi, inizia a guardare altre classi di quel tipo. Se sono plugin, controlla se qualcuno dei genitori implementa il ContainerFactoryPluginInterface. In caso contrario, fai da te per la tua classe e assicurati che il costruttore riceva ciò che si aspetta. Controlla anche la classe del gestore plugin responsabile e vedi quale fabbrica usa. 

In altri casi, ad esempio con classi TypedData come Tipo di campo, dare un'occhiata ad altri esempi nel nucleo. Se vedi altri utenti che utilizzano servizi caricati staticamente, molto probabilmente non è ancora pronto per l'iniezione, quindi dovrai fare lo stesso. Ma tieni d'occhio, perché questo potrebbe cambiare in futuro.