Drupal 8 Korrekt injeksjon av avhengighet ved bruk av DI

Som jeg er sikker på, vet du nå, er avhengighetsinjeksjon (DI) og Symfony servicebeholder viktige nye utviklingsfunksjoner i Drupal 8. Men selv om de begynner å bli bedre forstått i Drupal-utviklingssamfunnet, er det fortsatt mangel på mangel klarhet om hvordan du nøyaktig kan injisere tjenester i Drupal 8 klasser.

Mange eksempler snakker om tjenester, men de fleste dekker bare den statiske måten å laste dem på:

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

Dette er forståelig som riktig injeksjon tilnærming er mer verbose, og hvis du vet det allerede, heller boilerplate. Men den statiske tilnærmingen i ekte liv bør bare brukes i to tilfeller:

  • i .modul fil (utenfor en klassekontekst)
  • de sjeldne anledninger i en klassekontekst hvor klassen lastes uten servicebeholderbevissthet

Annet enn det, er injeksjonstjenester den beste praksisen, da det sikrer koblet kode og letter testingen.

I Drupal 8 er det noen spesifikasjoner om avhengighetsinjeksjon som du ikke kan forstå utelukkende fra en ren Symfony-tilnærming. Så i denne artikkelen skal vi se på noen eksempler på riktig konstruktørinjeksjon i Drupal 8. For dette formål, men også for å dekke alle grunnleggende, vil vi se på tre typer eksempler, for kompleksitet:

  • injisere tjenester i en annen av dine egne tjenester
  • injisere tjenester i ikke-service klasser
  • injisere tjenester i plugin klasser

Fortsatt, antagelsen er at du allerede vet hva DI er, hvilken hensikt den tjener og hvordan servicebeholderen støtter den. Hvis ikke, anbefaler jeg at du sjekker ut denne artikkelen først.

tjenester

Det er veldig enkelt å injisere tjenester i egen tjeneste. Siden du er den som definerer tjenesten, må du bare sende det som et argument til tjenesten du vil injisere. Tenk på følgende servicedefinisjoner:

tjenester: demo.demo_service: klasse: Drupal \ demo \ DemoService demo.another_demo_service: klasse: Drupal \ demo \ AnotherDemoService argumenter: ['@ demo.demo_service']

Her definerer vi to tjenester hvor den andre tar den første som et konstruktørargument. Så alt vi trenger å gjøre nå i AnotherDemoService klassen lagrer den som en lokal variabel:

klasse AnotherDemoService / ** * @var \ Drupal \ demo \ DemoService * / privat $ demoService; offentlig funksjon __construct (DemoService $ demoService) $ this-> demoService = $ demoService;  // Resten av metodene dine 

Og det er ganske mye det. Det er også viktig å nevne at denne tilnærmingen er nøyaktig den samme som i Symfony, så ingen endring her.

Ikke-service klasser

La oss nå se på klasser som vi ofte samhandler med, men det er ikke våre egne tjenester. For å forstå hvordan denne injeksjonen foregår, må du forstå hvordan klassene er løst og hvordan de blir opprettet. Men vi vil se det i praksis snart.

Controllers

Controller klasser er mest brukt for å kartlegge rutingsbaner til forretningslogikk. De skal holde seg tynne og delegere tyngre forretningslogikk til tjenester. Mange strekker seg ControllerBase klasse og få hjelpeprosesser til å hente felles tjenester fra containeren. Disse returneres imidlertid statisk.

Når et kontrolleringsobjekt blir opprettet (ControllerResolver :: createController), den ClassResolver er vant til å få en forekomst av klassifikasjons klassens definisjon. Oppløseren er oppmerksom på beholderen og returnerer en forekomst av kontrolleren hvis beholderen allerede har den. Omvendt instanserer den en ny og returnerer det. 

Og her er hvor injeksjonen vår finner sted: Hvis klassen blir løst, implementerer den ContainerAwareInterface, instantiation finner sted ved å bruke den statiske skape() metode på den klassen som mottar hele beholderen. Og vår ControllerBase klassen implementerer også ContainerAwareInterface.

Så la oss ta en titt på et eksempelkontroller som riktig injiserer tjenester ved hjelp av denne tilnærmingen (i stedet for å be om dem statisk):

/ ** * Definerer en kontroller til listeblokker. * / klasse BlockListController utvider EntityListController / ** * Temahantereren. * * @var \ Drupal \ Core \ Extension \ ThemeHandlerInterface * / beskyttet $ themeHandler; / ** * Konstruerer BlockListController. * * @param \ Drupal \ Core \ Extension \ ThemeHandlerInterface $ theme_handler * Temahantereren. * / offentlig funksjon __construct (ThemeHandlerInterface $ theme_handler) $ this-> themeHandler = $ theme_handler;  / ** * @inheritdoc * / offentlig statisk funksjon opprette (ContainerInterface $ container) return new static ($ container-> get ('theme_handler')); 

De EntityListController klassen gjør ingenting for våre formål her, så bare forestill deg det BlockListController strekker seg direkte ut ControllerBase klassen, som i sin tur utfører ContainerInjectionInterface.

Som vi sa, når denne kontrolleren er instantiated, den statiske skape() Metoden kalles. Formålet er å instansiere denne klassen og passere hvilke parametere den ønsker til klassekonstruktøren. Og siden beholderen er sendt til skape(), Det kan velge hvilke tjenester som skal be om og overføre til konstruktøren. 

Deretter må byggeren ganske enkelt motta tjenestene og lagre dem lokalt. Husk at det er dårlig praksis å injisere hele beholderen inn i klassen din, og du bør alltid begrense de tjenestene du injiserer til de du trenger. Og hvis du trenger for mange, vil du sannsynligvis gjøre noe galt.

Vi brukte denne kontrolleren eksempel å gå litt dypere inn i Drupal avhengighet injeksjon tilnærming og forstå hvordan konstruktør injeksjon fungerer. Det er også setter injeksjonsmuligheter ved å gjøre klasser beholder oppmerksom, men vi vil ikke dekke det her. La oss i stedet se på andre eksempler på klasser du kan samhandle med og hvor du skal injisere tjenester.

skjemaer

Skjemaer er et annet godt eksempel på klasser hvor du må injisere tjenester. Vanligvis strekker du enten FormBase eller ConfigFormBase klasser som allerede implementerer ContainerInjectionInterface. I dette tilfellet, hvis du tilsidesetter skape() og konstruktør metoder, kan du injisere hva du vil. Hvis du ikke ønsker å forlenge disse klassene, er alt du trenger å gjøre implementere dette grensesnittet selv og følg de samme trinnene vi så over med kontrolleren.

Som et eksempel, la oss ta en titt på SiteInformationForm som strekker seg ConfigFormBase og se hvordan det injiserer tjenester på toppen av config.factory dets foreldres behov:

klasse SiteInformationForm utvider ConfigFormBase ... offentlig funksjon __construct (ConfigFactoryInterface $ config_factory, AliasManagerInterface $ alias_manager, PathValidatorInterface $ path_validator, RequestContext $ request_context) foreldre :: __ konstruere ($ config_factory); $ this-> aliasManager = $ alias_manager; $ this-> pathValidator = $ path_validator; $ this-> requestContext = $ request_context;  / ** * @inheritdoc * / offentlig statisk funksjon opprette (ContainerInterface $ container) return new static ($ container-> get ('config.factory'), $ container-> get ('path.alias_manager') , $ container-> få ('path.validator'), $ container-> få ('router.request_context'));  ...

Som før, den skape() Metoden brukes til instantiation, som overfører til konstruktøren tjenesten som kreves av foreldreklassen, samt noen ekstra som den trenger på toppen.

Og dette er ganske mye hvordan den grunnleggende konstruktørinjeksjonen fungerer i Drupal 8. Den er tilgjengelig i nesten alle klassekontekster, lagre for noen der instantieringsdelen ennå ikke var løst på denne måten (for eksempel FieldType plugins). I tillegg er det et viktig delsystem som har noen forskjeller, men er avgjørende for å forstå: plugins.

plugins

Plug-systemet er en svært viktig Drupal 8-komponent som gir mye funksjonalitet. Så la oss se hvordan avhengighetsinsprøyting fungerer med plugin-klasser.

Den viktigste forskjellen i hvordan injeksjon håndteres med plugins, er grensesnittet plugin klasser må implementeres: ContainerFactoryPluginInterface. Årsaken er at plugins ikke er løst, men administreres av en plugin manager. Så når denne sjefen trenger å instantiere en av dens plugins, vil den gjøre det ved å bruke en fabrikk. Og vanligvis er denne fabrikken den ContainerFactory (eller en lignende variant av det). 

Så hvis vi ser på ContainerFactory :: CreateInstance (), vi ser det bortsett fra at beholderen blir sendt til det vanlige skape() metode, den $ konfigurasjon, $ plugin_id, og $ plugin_definition variabler overføres også (som er de tre grunnleggende parametrene hver plugin kommer med).

Så la oss se to eksempler på slike plugins som injiserer tjenester. Først, kjernen UserLoginBlock plugg inn (@Blokkere):

klasse UserLoginBlock utvider BlockBase implementerer ContainerFactoryPluginInterface ... offentlig funksjon __construct (array $ konfigurasjon, $ plugin_id, $ plugin_definition, RouteMatchInterface $ route_match) foreldre :: __ konstruere ($ konfigurasjon, $ plugin_id, $ plugin_definition); $ this-> routeMatch = $ route_match;  / ** * @inheritdoc * / offentlig statisk funksjon opprette (ContainerInterface $ container, array $ konfigurasjon, $ plugin_id, $ plugin_definition) return ny statisk ($ konfigurasjon, $ plugin_id, $ plugin_definition, $ container-> get 'current_route_match'));  ...

Som du kan se, implementerer den ContainerFactoryPluginInterface og skape() Metoden mottar de tre ekstra parametrene. Disse blir så passert i riktig rekkefølge til klassekonstruktøren, og fra beholderen blir det også bedt om en tjeneste. Dette er det mest grunnleggende, men likevel brukte, eksempelet på å injisere tjenester i plugin-klasser.

Et annet interessant eksempel er FileWidget plugg inn (@FieldWidget):

klasse FileWidget utvider WidgetBase implementerer ContainerFactoryPluginInterface / ** * @inheritdoc * / offentlig funksjon __construct ($ plugin_id, $ plugin_definition, FieldDefinitionInterface $ field_definition, array $ settings, array $ third_party_settings, ElementInfoManagerInterface $ element_info) foreldre :: __ konstruere plugin_id, $ plugin_definition, $ field_definition, $ settings, $ third_party_settings); $ this-> elementInfo = $ element_info;  / ** * @inheritdoc * / offentlig statisk funksjon opprette (ContainerInterface $ container, array $ konfigurasjon, $ plugin_id, $ plugin_definition) return ny statisk ($ plugin_id, $ plugin_definition, $ configuration ['field_definition'], $ konfigurasjon ['settings'], $ konfigurasjon ['third_party_settings'], $ container-> get ('element_info'));  ...

Som du kan se, er skape() Metoden mottar de samme parameterne, men klassekonstruktøren forventer ekstra de som er spesifikke for denne plugin-typen. Dette er ikke et problem. De kan vanligvis finnes i $ konfigurasjon utvalg av det aktuelle pluginet og passert derfra.

Så dette er de viktigste forskjellene når det gjelder å injisere tjenester i plugin-klasser. Det er et annet grensesnitt å implementere og noen ekstra parametere i skape() metode.

Konklusjon

Som vi har sett i denne artikkelen, er det flere måter vi kan få våre hender på tjenester i Drupal 8. Noen ganger må vi statisk be om dem. Men mesteparten av tiden burde vi ikke. Og vi har sett noen typiske eksempler på når og hvordan vi skal injisere dem i våre klasser i stedet. Vi har også sett de to hovedgrensesnittene klassene må implementere for å være instantiated med beholderen og være klar til injeksjon, så vel som forskjellen mellom dem.

Hvis du jobber i en klassekontekst, og du er usikker på hvordan du injiserer tjenester, må du begynne å se på andre klasser av den typen. Hvis de er plugins, sjekk om noen av foreldrene implementerer ContainerFactoryPluginInterface. Hvis ikke, gjør det selv for klassen din og sørg for at konstruktøren mottar det du forventer. Ta også en titt på ansvarlig plugin manager klasse og se hvilken fabrikk den bruker. 

I andre tilfeller, som med TypedData klasser som FieldType, ta en titt på andre eksempler i kjernen. Hvis du ser andre ved hjelp av statisk lastede tjenester, er det sannsynligvis ikke klar for injeksjon, så du må gjøre det samme. Men hold øye med, fordi dette kan endres i fremtiden.