Drupal 8 În mod corespunzător, injectarea dependențelor folosind DI

După cum sunt sigur că știți deja, injecția de dependență (DI) și containerul de servicii Symfony reprezintă noi caracteristici noi de dezvoltare ale Drupal 8. Cu toate acestea, deși încep să fie mai bine înțelese în comunitatea de dezvoltare Drupal, există încă o lipsă de claritate cu privire la modul exact de injectare a serviciilor în clasele Drupal 8.

Multe exemple vorbesc despre servicii, dar cele mai multe acoperă doar modul static de încărcare a acestora:

$ service = \ Drupal :: serviciu ('service_name');

Acest lucru este de înțeles deoarece abordarea adecvată a injectării este mai verbală, iar dacă o cunoașteți deja, mai degrabă boilerplate. Cu toate acestea, abordarea statică în viata reala ar trebui să fie utilizat numai în două cazuri:

  • în .modul fișier (în afara contextului de clasă)
  • acele ocazii rare într-un context de clasă în care clasa este încărcată fără conștientizarea containerului de serviciu

În afară de aceasta, serviciile de injectare reprezintă cea mai bună practică, deoarece asigură codul decuplat și facilitează testarea.

În Drupal 8 există câteva particularități legate de injectarea de dependență, pe care nu o veți putea înțelege exclusiv dintr-o abordare pur simfonică. Astfel, în acest articol vom examina câteva exemple de injecție corectă a constructorului în Drupal 8. În acest scop, dar și pentru a acoperi toate elementele de bază, vom examina trei tipuri de exemple, în ordinea complexității:

  • injectați servicii într-un alt serviciu propriu
  • injectarea de servicii în clasele non-service
  • injectarea serviciilor în clase de pluginuri

Mergând înainte, presupunerea este că știți deja ce este DI, ce scop servește și cum îl suportă containerul de servicii. Dacă nu, recomand să verificați mai întâi acest articol.

Servicii

Injectarea de servicii în propriul serviciu este foarte ușoară. Deoarece sunteți cel care definește serviciul, tot ce trebuie să faceți este să-l transmiteți ca argument pentru serviciul pe care doriți să-l injectați. Imaginați-vă următoarele definiții ale serviciului:

servicii: demo.demo_service: clasa: Drupal \ demo \ DemoService demo.another_demo_service: clasa: Drupal \ demo \ OtherDemoService argumente: ['@ demo.demo_service']

Aici definim două servicii unde a doua o ia pe prima ca argument constructor. Deci tot ce trebuie să facem acum în AnotherDemoService clasa este stocată ca variabilă locală:

clasa OtherDemoService / ** * @var \ Drupal \ demo \ DemoService * / privat $ demoService; funcția publică __construct (DemoService $ demoService) $ this-> demoService = $ demoService;  // Restul metodelor tale 

Și asta este destul de mult. De asemenea, este important să menționăm că această abordare este exact la fel ca și în Symfony, deci nu se schimbă aici.

Clasele non-de serviciu

Acum, să aruncăm o privire la clasele cu care interacționăm adesea, dar care nu sunt serviciile noastre. Pentru a înțelege cum are loc această injecție, trebuie să înțelegeți modul în care clasele sunt rezolvate și modul în care acestea sunt instanțiate. Dar vom vedea acest lucru în curând.

controlerele

Clasele de control sunt utilizate în principal pentru a cartografia căile de rutare în logica de afaceri. Ele ar trebui să rămână subțiri și să delege o logică de afaceri mai grea pentru servicii. Multe se extind ControllerBase clasa și a obține unele metode de ajutor pentru a recupera servicii comune de la container. Totuși, acestea sunt returnate în mod static.

Când se creează un obiect de controler (ControllerResolver :: createController), ClassResolver este folosit pentru a obține o instanță a definiției clasei de controler. Resolverul este conștient de container și returnează o instanță a controlerului dacă containerul o are deja. Dimpotrivă, instanțiază una nouă și returnează acest lucru. 

Iată aici unde are loc injectarea noastră: dacă clasa care este rezolvată implementează ContainerAwareInterface, instantierea are loc prin utilizarea staticului crea() pe acea clasă care primește întregul container. Și al nostru ControllerBase clasa implementează de asemenea ContainerAwareInterface.

Deci, să aruncăm o privire la un controler de exemplu care injectează corect serviciile care utilizează această abordare (în loc să le solicite în mod static):

/ ** * Definește un controler pentru a lista blocurile. * / clasa BlockListController extinde EntityListController / ** * Handler-ul temei. * * @var \ Drupal \ Core \ Extension \ ThemeHandlerInterface * / protejat $ themeHandler; / ** * Construiește BlockListController. * * @param \ Drupal \ Core \ Extension \ ThemeHandlerInterface $ theme_handler * Managerul temelor. * / funcția publică __construct (ThemeHandlerInterface $ theme_handler) $ this-> themeHandler = $ theme_handler;  / ** * @inheritdoc * / funcția statică publică crea (ContainerInterface $ container) retur static nou ($ container-> get ('theme_handler')); 

EntityListController clasa nu face nimic pentru scopurile noastre aici, așa că imaginați-vă asta BlockListController direct extinde ControllerBase clasa, care la rândul ei implementează ContainerInjectionInterface.

Așa cum am spus, atunci când acest controler este instanțiat, static crea() se numește metoda. Scopul său este să instanțiate această clasă și să treacă parametrii pe care îi dorește constructorului de clasă. Și din moment ce containerul este trecut crea(), acesta poate alege ce servicii să solicite și să treacă de-a lungul constructorului. 

Apoi, constructorul trebuie doar să primească serviciile și să le stocheze local. Rețineți că este o practică nepotrivită de a injecta întregul container în clasa dvs. și trebuie să limitați întotdeauna serviciile pe care le injectați la cele de care aveți nevoie. Și dacă aveți nevoie de prea multe, probabil că faceți ceva greșit.

Am folosit acest exemplu de controler pentru a merge mai adânc în abordarea Drupal de injectare a dependenței și a înțelege cum funcționează injecția constructorului. Există, de asemenea, posibilități de injectare a setterului, făcând conștientizarea clasei container, dar nu vom acoperi aici. Să examinăm în schimb alte exemple de clase pe care le puteți interacționa și în care trebuie să injectați servicii.

Formulare

Formularele reprezintă un alt exemplu de clasă în care trebuie să injectați servicii. De obicei, fie extindeți FormBase sau ConfigFormBase clase care pun în aplicare deja ContainerInjectionInterface. În acest caz, dacă ignorați crea() și metode de constructor, puteți injecta orice doriți. Dacă nu doriți să extindeți aceste clase, tot ce trebuie să faceți este să implementați această interfață și să urmați aceiași pași pe care i-am văzut mai sus cu controlerul.

De exemplu, să aruncăm o privire la SiteInformationForm care extinde ConfigFormBase și să vedem cum se injectează serviciile deasupra config.factory nevoia sa materna:

clasa SiteInformationForm extinde ConfigFormBase ... funcția publică __construct (ConfigFactoryInterface $ config_factory, AliasManagerInterface $ alias_manager, PathValidatorInterface $ cale_validator, RequestContext $ request_context) părinte :: __ construct ($ config_factory); $ this-> aliasManager = $ alias_manager; $ this-> pathValidator = $ path_validator; $ this-> requestContext = $ request_context;  / ** * @inheritdoc * / crearea funcției statice publice (ContainerInterface $ container) retur static nou ($ container-> get ('config.factory'), $ container-> get ('path.alias_manager') , $ container-> get ('path.validator'), $ container-> get ('router.request_context'));  ...

Ca și înainte, crea() se utilizează metoda pentru instanțiere, care transmite constructorului serviciul solicitat de clasa parentală, precum și unele suplimentare pe care le are nevoie de partea superioară.

Și acest lucru este destul de mult modul în care injectorul constructor de bază funcționează în Drupal 8. Este disponibil în aproape toate contextele de clasă, cu excepția câtorva în care partea de instanțiere nu a fost încă rezolvată în acest mod (de exemplu, pluginurile FieldType). În plus, există un subsistem important care are unele diferențe, dar este extrem de important să înțelegeți: pluginurile.

Plugin-uri

Sistemul de pluginuri este o componentă foarte importantă Drupal 8, care oferă o mulțime de funcționalități. Deci, să vedem cum funcționează injecția de dependență cu clase de pluginuri.

Cea mai importantă diferență în modul în care se administrează injectarea cu plugin-uri este interfața cu clasele de plugin care trebuie implementate: ContainerFactoryPluginInterface. Motivul este că pluginurile nu sunt rezolvate, dar sunt gestionate de un manager de pluginuri. Deci, atunci când acest manager trebuie să instanțiate unul dintre pluginurile sale, va face acest lucru folosind o fabrică. Și, de obicei, această fabrică este ContainerFactory (sau o variație similară a acesteia). 

Deci, dacă ne uităm ContainerFactory :: createInstance (), vedem că, în afara containerului, trecem la obișnuit crea() metodă, configurare $, $ plugin_id, și $ plugin_definition sunt transmise și variabilele (care sunt cei trei parametri de bază ai fiecărui plugin).

Așadar, să vedem două exemple de astfel de pluginuri care injectează servicii. În primul rând, nucleul UserLoginBlock conecteaza (@Bloc):

classLoginBlock extinde implementările BlockBase ContainerFactoryPluginInterface ... funcția publică __construct (array $ configuration, $ plugin_id, $ plugin_definition, RouteMatchInterface $ route_match) părinte :: __construct ($ configuration, $ plugin_id, $ plugin_definition); $ acest-> routeMatch = $ route_match;  / ** * @inheritdoc * / create funcția statică publică (ContainerInterface $ container, array $ configuration, $ plugin_id, $ plugin_definition) retur static nou ($ configuration, $ plugin_id, $ plugin_definition, $ container-> 'current_route_match'));  ...

După cum puteți vedea, acesta implementează ContainerFactoryPluginInterface si crea() metoda primește acești trei parametri suplimentari. Acestea sunt apoi transmise în ordinea corectă constructorului de clasă, iar din container este solicitat și trecut un serviciu. Acesta este exemplul cel mai de bază, dar cel mai frecvent utilizat, de injectare a serviciilor în clase de pluginuri.

Un alt exemplu interesant este FileWidget conecteaza (@FieldWidget):

ClassWidget extinde aplicațiile WidgetBase ContainerFactoryPluginInterface / ** * @inheritdoc * / funcția publică __construct ($ plugin_id, $ plugin_definition, FieldDefinitionInterface $ field_definition, array $ set, array $ third_party_settings, ElementInfoManagerInterface $ element_info) parent :: __ construct plugin_id, $ plugin_definition, $ field_definition, $ setări, $ third_party_settings); $ this-> elementInfo = $ element_info;  / ** * @inheritdoc * / crea funcția publică statică (ContainerInterface $ container, array $ configuration, $ plugin_id, $ plugin_definition) retur static nou ($ plugin_id, $ plugin_definition, $ configuration ['field_definition' configurare ['setări'], $ configuration ['third_party_settings'], $ container-> get ('element_info'));  ...

După cum puteți vedea, crea() metoda primește aceiași parametri, dar constructorul de clasă se așteaptă ca acei parametri să fie specifici acestui tip de plugin. Aceasta nu este o problemă. Ele pot fi de obicei găsite în interiorul configurare $ array de plugin-ul respectiv și a trecut de acolo.

Deci, acestea sunt principalele diferențe atunci când vine vorba de injectarea de servicii în clase de plugin. Există o interfață diferită de implementat și câțiva parametri suplimentari în crea() metodă.

Concluzie

După cum am văzut în acest articol, există mai multe modalități prin care putem să ne ocupăm de servicii în Drupal 8. Uneori trebuie să le cerem în mod static. Cu toate acestea, de cele mai multe ori nu ar trebui. Și am văzut câteva exemple tipice despre momentul și modul în care ar trebui să le injectăm în clasă. De asemenea, am văzut cele două interfețe principale pe care clasele trebuie să le pună în aplicare pentru a fi instanțiate cu containerul și să fie gata pentru injecție, precum și diferența dintre acestea.

Dacă lucrați într-un context de clasă și nu sunteți siguri cum să injectați servicii, începeți să căutați alte clase de acest tip. Dacă acestea sunt pluginuri, verificați dacă oricare dintre părinți implementează ContainerFactoryPluginInterface. Dacă nu, faceți-o singură pentru clasă și asigurați-vă că constructorul primește ceea ce se așteaptă. De asemenea, verificați responsabilitatea managerului de plugin și vedeți ce fabrica utilizează. 

În alte cazuri, cum ar fi cu clase TypedData cum ar fi FieldType, aruncăm o privire la alte exemple în esență. Dacă vedeți alții utilizând servicii încărcate static, cel mai probabil nu este încă pregătit pentru injectare, deci va trebui să faceți același lucru. Dar păstrați-vă un ochi, pentru că acest lucru s-ar putea schimba în viitor.

Cod