Comme vous le savez sans doute à présent, l'injection de dépendance et le conteneur de services Symfony sont d'importantes nouvelles fonctionnalités de développement de Drupal 8. Cependant, même si elles commencent à être mieux comprises par la communauté du développement Drupal, de clarté sur la façon exacte d'injecter des services dans Drupal 8 classes.
De nombreux exemples parlent de services, mais la plupart ne couvrent que la façon statique de les charger:
$ service = \ Drupal :: service ('nom_service');
Cela est compréhensible car la méthode d’injection appropriée est plus détaillée et, si vous la connaissez déjà, plutôt passe-partout. Cependant, l’approche statique dans vrai vie doit être utilisé que dans deux cas:
.module
fichier (en dehors d'un contexte de classe)En dehors de cela, l’injection de services est la meilleure pratique car elle garantit un code découplé et facilite les tests..
Dans Drupal 8, il existe certaines spécificités sur l'injection de dépendance que vous ne pourrez pas comprendre uniquement à partir d'une approche purement Symfony. Dans cet article, nous allons donc examiner quelques exemples d’injection de constructeur dans Drupal 8. Pour cela, mais aussi pour couvrir toutes les bases, nous allons examiner trois types d’exemples, par ordre de complexité:
À l’avenir, nous partons du principe que vous savez déjà ce qu'est le DI, à quoi sert-il et comment le conteneur de services le prend en charge. Si non, je recommande de vérifier cet article en premier.
L'injection de services dans votre propre service est très facile. Puisque vous définissez le service, tout ce que vous avez à faire est de le passer comme argument au service que vous souhaitez injecter. Imaginez les définitions de service suivantes:
services: demo.demo_service: classe: Drupal \ demo \ DemoService demo.another_demo_service: classe: Drupal \ demo \ AnotherDemoService arguments: ['@ demo.demo_service']
Nous définissons ici deux services, le second prenant le premier comme argument constructeur. Donc, tout ce que nous avons à faire maintenant dans le AnotherDemoService
la classe est le stocker comme une variable locale:
class AnotherDemoService / ** * @var \ Drupal \ demo \ DemoService * / private $ demoService; fonction publique __construct (DemoService $ demoService) $ this-> demoService = $ demoService; // le reste de vos méthodes
Et c'est à peu près tout. Il est également important de mentionner que cette approche est exactement la même que dans Symfony, donc aucun changement ici..
Examinons maintenant les classes avec lesquelles nous interagissons souvent mais qui ne sont pas nos propres services. Pour comprendre comment se déroule cette injection, vous devez comprendre comment les classes sont résolues et comment elles sont instanciées. Mais nous verrons que dans la pratique bientôt.
Les classes de contrôleur sont principalement utilisées pour mapper les chemins de routage sur la logique métier. Ils sont censés rester minces et déléguer une logique métier plus lourde aux services. Beaucoup prolongent la ControllerBase
classe et obtenir des méthodes d'assistance pour récupérer des services communs à partir du conteneur. Cependant, ceux-ci sont retournés statiquement.
Lorsqu'un objet contrôleur est en cours de création (ControllerResolver :: createController
), la ClassResolver
est utilisé pour obtenir une instance de la définition de la classe du contrôleur. Le résolveur est conscient du conteneur et renvoie une instance du contrôleur si le conteneur en dispose déjà. Inversement, il en instancie un nouveau et renvoie celui-ci..
Et voici où notre injection a lieu: si la classe en cours de résolution met en œuvre le ContainerAwareInterface
, l'instanciation a lieu en utilisant le statique créer()
méthode sur cette classe qui reçoit le conteneur entier. Et notre ControllerBase
la classe implémente également le ContainerAwareInterface
.
Voyons donc un exemple de contrôleur injectant correctement les services utilisant cette approche (au lieu de les demander de manière statique):
/ ** * Définit un contrôleur pour lister les blocs. * / class BlockListController étend EntityListController / ** * Le gestionnaire de thème. * * @var \ Drupal \ Core \ Extension \ ThemeHandlerInterface * / protected $ themeHandler; / ** * Construit le BlockListController. * * @param \ Drupal \ Core \ Extension \ ThemeHandlerInterface $ theme_handler * Le gestionnaire de thème. * / public function __construct (ThemeHandlerInterface $ theme_handler) $ this-> themeHandler = $ theme_handler; / ** * @inheritdoc * / fonction statique publique create (ContainerInterface $ container) retourne new static ($ conteneur-> get ('theme_handler'));
le EntityListController
classe ne fait rien pour nos besoins ici, alors imaginez que BlockListController
étend directement le ControllerBase
classe, qui à son tour implémente la ContainerInjectionInterface
.
Comme nous l'avons dit, lorsque ce contrôleur est instancié, le statique créer()
méthode est appelée. Son but est d'instancier cette classe et de transmettre tous les paramètres souhaités au constructeur de la classe. Et puisque le conteneur est passé à créer()
, il peut choisir les services à demander et les transmettre au constructeur.
Ensuite, le constructeur doit simplement recevoir les services et les stocker localement. N'oubliez pas qu'il est déconseillé d'injecter l'intégralité du conteneur dans votre classe et que vous devez toujours limiter les services que vous injectez à ceux dont vous avez besoin. Et si vous avez besoin de trop, vous faites probablement quelque chose de mal.
Nous avons utilisé cet exemple de contrôleur pour approfondir un peu l'approche par injection de dépendance Drupal et comprendre le fonctionnement de l'injection de constructeur. Il existe également des possibilités d’injection de setter en rendant les classes plus sensibles aux conteneurs, mais nous ne couvrirons pas cela ici. Voyons plutôt d'autres exemples de classes avec lesquelles vous pouvez interagir et dans lesquelles vous devriez injecter des services..
Les formulaires sont un autre excellent exemple de cours dans lesquels vous devez utiliser des services. Habituellement, vous prolongez le FormBase
ou ConfigFormBase
classes qui implémentent déjà la ContainerInjectionInterface
. Dans ce cas, si vous remplacez le créer()
et méthodes constructeur, vous pouvez injecter ce que vous voulez. Si vous ne voulez pas étendre ces classes, tout ce que vous avez à faire est d'implémenter cette interface vous-même et de suivre les étapes décrites ci-dessus avec le contrôleur..
Par exemple, jetons un coup d’œil à la SiteInformationForm
qui étend la ConfigFormBase
et voyez comment il injecte des services en plus de la config.factory
son parent a besoin de:
class SiteInformationForm étend ConfigFormBase … public function __construct (ConfigFactoryInterface $ config_factory, AliasManagerInterface $ alien_manager, PathValidatorInterface $ path_validator, RequestContext $ request_context) parent :: __ construct ($ config_factory); $ this-> aliasManager = $ alias_manager; $ this-> pathValidator = $ path_validator; $ this-> requestContext = $ request_context; / ** * @inheritdoc * / fonction statique publique create (ContainerInterface $ container) retourne new static ($ conteneur-> get ('config.factory'), $ conteneur-> get ('path.alias_manager') , $ conteneur-> get ('path.validator'), $ conteneur-> get ('router.request_context')); …
Comme avant, le créer()
La méthode est utilisée pour l'instanciation, qui transmet au constructeur le service requis par la classe parente, ainsi que certains services supplémentaires dont il a besoin par dessus.
Et c’est à peu près comme cela fonctionne avec l’injection de base dans Drupal 8. Elle est disponible dans presque tous les contextes de classe, à l'exception de quelques-uns dans lesquels la partie instanciation n'a pas encore été résolue de cette manière (par exemple, des plugins FieldType). De plus, il existe un sous-système important qui présente quelques différences mais qui est d'une importance cruciale à comprendre: les plugins.
Le système de plug-in est un composant très important de Drupal 8 qui alimente de nombreuses fonctionnalités. Voyons donc comment fonctionne l'injection de dépendance avec les classes de plugin.
La différence la plus importante dans la manière dont l'injection est gérée avec des plugins est la nécessité pour les classes de plugins d'interface d'implémenter: ContainerFactoryPluginInterface
. La raison en est que les plugins ne sont pas résolus mais gérés par un gestionnaire de plugins. Ainsi, lorsque ce gestionnaire devra instancier l'un de ses plugins, il le fera à l'aide d'une usine. Et généralement, cette usine est le ContainerFactory
(ou une variante similaire).
Donc si on regarde ContainerFactory :: createInstance ()
, nous voyons cela en dehors du conteneur étant passé à l'habituel créer()
méthode, la $ configuration
, $ plugin_id
, et $ plugin_definition
les variables sont également passées (quels sont les trois paramètres de base fournis avec chaque plugin).
Voyons donc deux exemples de tels plugins qui injectent des services. Tout d'abord, le noyau UserLoginBlock
brancher (@Bloc
):
class UserLoginBlock s'étend BlockBase implémente ContainerFactoryPluginInterface … public function __construct (tableau $ configuration, $ plugin_id, $ plugin_definition, RouteMatchInterface $ route_match) parent :: __ construct ($ configuration, $ plugin_id, $ plugin_definition); $ this-> routeMatch = $ route_match; / ** * @inheritdoc * / fonction statique publique create (ContainerInterface $ conteneur, configuration $ de tableau, $ plugin_id, $ plugin_definition) retourne new statique ($ configuration, $ plugin_definition, $ conteneur-> get ( 'current_route_match')); …
Comme vous pouvez le voir, il met en œuvre le ContainerFactoryPluginInterface
et le créer()
méthode reçoit ces trois paramètres supplémentaires. Celles-ci sont ensuite passées dans le bon ordre au constructeur de classe et, à partir du conteneur, un service est également demandé et transmis. C’est l’exemple le plus élémentaire, bien que couramment utilisé, d’injection de services dans des classes de plug-ins..
Un autre exemple intéressant est le FileWidget
brancher (@FieldWidget
):
classe FileWidget s'étend WidgetBase implémente ContainerFactoryPluginInterface / ** * @inheritdoc * / public function __construct ($ plugin_id, $ plugin_definition, FieldDefinitionInterface $ field_ment, date_ment plugin_id, $ plugin_definition, $ field_definition, $ settings, $ third_party_settings); $ this-> elementInfo = $ element_info; / ** * @inheritdoc * / public static function create (ContainerInterface $ conteneur, configuration $ tableau, $ plugin_id, $ plugin_definition) retour nouvelle static ($ plugin_id, $ plugin_definition, $ configuration ['field_definition']], $ configuration ['settings'], $ configuration ['third_party_settings'], $ conteneur-> get ('element_info')); …
Comme vous pouvez le voir, le créer()
méthode reçoit les mêmes paramètres, mais le constructeur de la classe attend ceux supplémentaires spécifiques à ce type de plugin. Ce n'est pas un problème. Ils peuvent généralement être trouvés à l'intérieur du $ configuration
tableau de ce plugin particulier et transmis à partir de là.
Voilà donc les principales différences en matière d’injection de services dans des classes de plug-ins. Il y a une interface différente à implémenter et quelques paramètres supplémentaires dans le créer()
méthode.
Comme nous l’avons vu dans cet article, Drupal 8 permet de se procurer les services de maintes façons. Nous devons parfois les solliciter statiquement. Cependant, la plupart du temps, nous ne devrions pas. Et nous avons vu quelques exemples typiques de quand et comment nous devrions plutôt les injecter dans nos classes. Nous avons également vu les deux interfaces principales que les classes doivent implémenter pour pouvoir être instanciées avec le conteneur et être prêtes à être injectées, ainsi que la différence entre elles..
Si vous travaillez dans un contexte de classe et que vous ne savez pas comment injecter des services, commencez à regarder d'autres classes de ce type. Si ce sont des plugins, vérifiez si l’un des parents met en œuvre la ContainerFactoryPluginInterface
. Sinon, faites-le vous-même pour votre classe et assurez-vous que le constructeur reçoit ce qu'il attend. Consultez également la classe responsable du gestionnaire de plug-in et voyez quelle usine elle utilise.
Dans d’autres cas, comme avec les classes TypedData comme le Type de champ
, Jetez un oeil à d'autres exemples dans le noyau. Si vous voyez d'autres personnes utiliser des services chargés de manière statique, il est fort probable que l'injection ne soit pas encore prête, vous devrez donc en faire autant. Mais gardez un oeil sur, car cela pourrait changer à l'avenir.