Drupal 8 Depingen op de juiste manier injecteren met DI

Zoals u ongetwijfeld weet, zijn afhankelijkheidsinjectie (DI) en de Symfony-servicecontainer belangrijke nieuwe ontwikkelingsfuncties van Drupal 8. Hoewel ze echter steeds beter worden begrepen in de ontwikkelingsgemeenschap van Drupal, is er nog steeds gebrek aan van duidelijkheid over hoe precies services in Drupal 8-klassen te injecteren.

Veel voorbeelden hebben het over services, maar de meeste behandelen alleen de statische manier om ze te laden:

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

Dit is begrijpelijk, omdat de juiste injectiebenadering meer uitgebreid is, en als je het al weet, nogal overzichtelijk. De statische benadering in echte leven mag alleen in twee gevallen worden gebruikt:

  • in de .module bestand (buiten een klassencontext)
  • die zeldzame gevallen binnen een klassencontext waarin de klasse wordt geladen zonder het bewustzijn van de servicecontainer

Afgezien daarvan is injecteren de beste methode, omdat hiermee de ontkoppelde code wordt gewaarborgd en het testen wordt vereenvoudigd.

In Drupal 8 zijn er enkele specifieke kenmerken met betrekking tot afhankelijkheidsinjectie die u niet alleen kunt begrijpen vanuit een pure Symfony-aanpak. Daarom gaan we in dit artikel enkele voorbeelden van de juiste constructorinjectie in Drupal 8 bekijken. Hiertoe, maar ook om alle basisprincipes te behandelen, zullen we drie soorten voorbeelden bekijken, in volgorde van complexiteit:

  • diensten injecteren in een andere van uw eigen diensten
  • diensten injecteren in niet-serviceklassen
  • services injecteren in plugin-klassen

Voor de toekomst is de aanname dat je al weet wat DI is, welk doel het dient en hoe de service-container dit ondersteunt. Zo niet, raad ik aan dit artikel eerst te lezen.

Diensten

Het injecteren van diensten in uw eigen service is heel eenvoudig. Omdat jij degene bent die de service definieert, hoef je het alleen maar als een argument door te geven aan de service die je wilt injecteren. Stel u de volgende servicedefinities voor:

services: demo.demo_service: class: Drupal \ demo \ DemoService demo.another_demo_service: class: Drupal \ demo \ AnotherDemoService arguments: ['@ demo.demo_service']

Hier definiëren we twee services waarbij de tweede de eerste als constructorargument neemt. Dus alles wat we nu moeten doen in de AnotherDemoService class slaat het op als een lokale variabele:

class AnotherDemoService / ** * @var \ Drupal \ demo \ DemoService * / private $ demoService; openbare functie __construct (DemoService $ demoService) $ this-> demoService = $ demoService;  // De rest van uw methoden 

En dat is zo ongeveer het. Het is ook belangrijk om te vermelden dat deze benadering precies hetzelfde is als in Symfony, dus hier geen verandering.

Niet-serviceklassen

Laten we nu kijken naar lessen waar we vaak mee communiceren, maar die niet onze eigen diensten zijn. Om te begrijpen hoe deze injectie plaatsvindt, moet u weten hoe de klassen zijn opgelost en hoe deze worden geïnstantieerd. Maar dat zullen we binnenkort in de praktijk zien.

Controllers

Controller-klassen worden meestal gebruikt voor het toewijzen van routeringspaden naar bedrijfslogica. Ze moeten dun blijven en zwaardere bedrijfslogica delegeren aan services. Velen verlengen de ControllerBase class en verkrijg enkele hulpmethoden om algemene services uit de container op te halen. Deze worden echter statisch geretourneerd.

Wanneer een controllerobject wordt gemaakt (ControllerResolver :: createController), de ClassResolver wordt gebruikt om een ​​instantie van de klasse-definitie van de controller te krijgen. De resolver is containerbewust en retourneert een exemplaar van de controller als de container deze al heeft. Omgekeerd maakt het een nieuwe en retourneert dat. 

En hier vindt onze injectie plaats: als de klasse die wordt opgelost de instrumenten implementeert ContainerAwareInterface, de instantiatie vindt plaats door gebruik te maken van de statische create () methode voor die klasse die de volledige container ontvangt. En onze ControllerBase klasse implementeert ook de ContainerAwareInterface.

Laten we dus eens kijken naar een voorbeeldcontroller die op de juiste manier diensten injecteert met deze benadering (in plaats van hem statisch te vragen):

/ ** * Definieert een controller om blokken te vermelden. * / class BlockListController breidt EntityListController uit / ** * De themahandler. * * @var \ Drupal \ Core \ Extension \ ThemeHandlerInterface * / protected $ themeHandler; / ** * Construeert de BlockListController. * * @param \ Drupal \ Core \ Extension \ ThemeHandlerInterface $ theme_handler * De themahandler. * / public function __construct (ThemeHandlerInterface $ theme_handler) $ this-> themeHandler = $ theme_handler;  / ** * @inheritdoc * / public static function create (ContainerInterface $ container) return new static ($ container-> get ('theme_handler')); 

De EntityListController klas doet hier niets voor onze doeleinden, dus stel je dat eens voor BlockListController verlengt direct de ControllerBase klasse, die op zijn beurt de ContainerInjectionInterface.

Zoals we al zeiden, wanneer deze controller wordt geïnstantieerd, is de statische create () methode wordt genoemd. Het doel is om deze klasse te instantiëren en alle parameters door te geven die de klasse-constructor wil. En aangezien de container wordt doorgegeven aan create (), het kan kiezen welke services moeten worden aangevraagd en doorgeven aan de constructor. 

Vervolgens moet de constructor de services gewoon ontvangen en lokaal opslaan. Onthoud dat het een slechte gewoonte is om de hele container in uw klas te injecteren, en u moet altijd de diensten die u injecteert, beperken tot degene die u nodig hebt. En als u er te veel nodig heeft, doet u waarschijnlijk iets verkeerd.

We hebben dit controllervoorbeeld gebruikt om een ​​beetje dieper in te gaan op de Drupal-afhankelijkheidsinjectie en te begrijpen hoe constructorinjectie werkt. Er zijn ook setter-injectiemogelijkheden door klassencontainers bewust te maken, maar we zullen dat hier niet behandelen. Laten we in plaats daarvan eens kijken naar andere voorbeelden van klassen waarmee u in contact kunt komen en waarin u services moet injecteren.

vormen

Formulieren zijn een ander geweldig voorbeeld van klassen waar je services moet injecteren. Meestal verleng je de FormBase of ConfigFormBase klassen die het programma al implementeren ContainerInjectionInterface. In dit geval, als u de create () en constructormethoden, je kunt injecteren wat je wilt. Als u deze klassen niet wilt uitbreiden, hoeft u alleen deze interface zelf te implementeren en dezelfde stappen te volgen die we hierboven met de controller hebben gezien..

Laten we als voorbeeld eens kijken naar de SiteInformationForm waardoor de ConfigFormBase en zie hoe het services toevoegt bovenop de config.factory zijn ouder heeft nodig:

class SiteInformationForm breidt ConfigFormBase uit ... 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 * / openbare statische functie maken (ContainerInterface $ container) retourneer nieuwe statische ($ container-> get ('config.factory'), $ container-> get ('path.alias_manager') , $ container-> get ('path.validator'), $ container-> get ('router.request_context'));  ...

Zoals eerder, de create () methode wordt gebruikt voor de instantiatie, die aan de constructor de service doorgeeft die de ouderklasse vereist, evenals enkele extra die het nodig heeft bovenop.

En dit is ongeveer hoe de basisconstructeurinjectie werkt in Drupal 8. Het is beschikbaar in bijna alle klassencontexten, behalve enkele waarin het instantiatiedeel op deze manier nog niet was opgelost (bijvoorbeeld FieldType-plug-ins). Daarnaast is er een belangrijk subsysteem dat enkele verschillen heeft maar van cruciaal belang is om te begrijpen: plug-ins.

plugins

Het plug-insysteem is een zeer belangrijke Drupal 8-component die veel functionaliteit mogelijk maakt. Laten we dus eens kijken hoe afhankelijkheidsinjectie werkt met plugin-klassen.

Het belangrijkste verschil in de manier waarop injectie wordt afgehandeld met plug-ins, is dat de klassen voor interface-plug-ins moeten worden geïmplementeerd: ContainerFactoryPluginInterface. De reden is dat plug-ins niet worden opgelost, maar worden beheerd door een plugin-manager. Dus wanneer deze manager een van de plug-ins moet converteren, gebeurt dit met een fabriek. En meestal is deze fabriek de ContainerFactory (of een vergelijkbare variant ervan). 

Dus als we kijken ContainerFactory :: CreateInstance (), we zien dat afgezien van de container die wordt doorgegeven aan de gebruikelijke create () methode, de $ configuratie, $ plugin_id, en $ plugin_definition variabelen worden ook doorgegeven (dit zijn de drie basisparameters voor elke plug-in).

Laten we dus twee voorbeelden bekijken van dergelijke plug-ins die services injecteren. Ten eerste de kern UserLoginBlock inpluggen (@Blok):

class UserLoginBlock breidt BlockBase-implementaties uit ContainerFactoryPluginInterface ... public function __construct (array $ configuratie, $ plugin_id, $ plugin_definition, RouteMatchInterface $ route_match) parent :: __ construct ($ configuration, $ plugin_id, $ plugin_definition); $ this-> routeMatch = $ route_match;  / ** * @inheritdoc * / openbare statische functie maken (ContainerInterface $ container, array $ configuratie, $ plugin_id, $ plugin_definition) retourneer nieuwe statische ($ configuratie, $ plugin_id, $ plugin_definition, $ container-> get ( 'current_route_match'));  ...

Zoals je kunt zien, implementeert het de ContainerFactoryPluginInterface en de create () methode ontvangt die drie extra parameters. Deze worden vervolgens in de juiste volgorde doorgegeven aan de klasse-constructor en uit de container wordt ook een service aangevraagd en doorgegeven. Dit is het meest eenvoudige, maar veelgebruikte, voorbeeld van injectieservices in plugin-klassen.

Een ander interessant voorbeeld is de FileWidget inpluggen (@FieldWidget):

class FileWidget breidt WidgetBase implementeert 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 * / openbare statische functie maken (ContainerInterface $ -container, array $ -configuratie, $ plugin_id, $ plugin_definition) retourneer nieuwe statische ($ plugin_id, $ plugin_definition, $ configuration ['field_definition'], $ configuratie ['instellingen'], $ configuratie ['third_party_settings'], $ container-> get ('element_info'));  ...

Zoals u kunt zien, de create () methode ontvangt dezelfde parameters, maar de klasse-constructor verwacht extra exemplaren die specifiek zijn voor dit type plug-in. Dit is geen probleem. Ze zijn meestal te vinden binnen de $ configuratie array van die specifieke plug-in en wordt vanaf daar doorgegeven.

Dit zijn dus de belangrijkste verschillen als het gaat om het injecteren van services in plugin-klassen. Er is een andere interface om te implementeren en enkele extra parameters in de create () methode.

Conclusie

Zoals we in dit artikel hebben gezien, zijn er een aantal manieren waarop we diensten in Drupal 8 kunnen verkrijgen. Soms moeten we ze statisch aanvragen. Meestal zouden we dat echter niet moeten doen. En we hebben enkele typische voorbeelden gezien van wanneer en hoe we ze in onze lessen zouden moeten injecteren. We hebben ook de twee belangrijkste interfaces gezien die de klassen moeten implementeren om te worden geïnstantieerd met de container en klaar te zijn voor injectie, evenals het verschil tussen hen.

Als u in een klascontext werkt en niet zeker weet hoe u services moet injecteren, begin dan met het bekijken van andere klassen van dat type. Als het plug-ins zijn, controleer dan of een van de ouders de ContainerFactoryPluginInterface. Zo niet, doe het dan zelf voor je klas en zorg ervoor dat de constructeur ontvangt wat hij verwacht. Bekijk ook de verantwoordelijke klasse voor plugin-managers en bekijk welke fabriek deze gebruikt. 

In andere gevallen, zoals met TypedData-klassen zoals de Veld soort, bekijk andere voorbeelden in de kern. Als u anderen ziet die statisch geladen services gebruiken, is deze waarschijnlijk nog niet klaar voor injectie, dus u moet hetzelfde doen. Maar let goed op, want dit kan in de toekomst veranderen.