Diese Präsentation wurde erfolgreich gemeldet.
Die SlideShare-Präsentation wird heruntergeladen. ×

Drupal 8 Services And Dependency Injection

Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Nächste SlideShare
Drupal 8 Services
Drupal 8 Services
Wird geladen in …3
×

Hier ansehen

1 von 81 Anzeige

Drupal 8 Services And Dependency Injection

Herunterladen, um offline zu lesen

Using the service manager is an essential part of a Drupal 8 developers toolkit and understanding it not only helps development, but can also allow you to create modules that can be easily used by other developers. There are numerous code examples out there that talk about using this or that service, so I'll look at how to go from "\Drupal::service('thing');" to finding and using services within Drupal 8. I will look at creating custom services to use within your own modules and provide injectable dependencies for other modules. I will also show how to override services to provide your own functionality to existing services. All code shown will be real examples that you can take away and use in your own projects.

Given at DrupalCamp London 2018

Using the service manager is an essential part of a Drupal 8 developers toolkit and understanding it not only helps development, but can also allow you to create modules that can be easily used by other developers. There are numerous code examples out there that talk about using this or that service, so I'll look at how to go from "\Drupal::service('thing');" to finding and using services within Drupal 8. I will look at creating custom services to use within your own modules and provide injectable dependencies for other modules. I will also show how to override services to provide your own functionality to existing services. All code shown will be real examples that you can take away and use in your own projects.

Given at DrupalCamp London 2018

Anzeige
Anzeige

Weitere Verwandte Inhalte

Diashows für Sie (20)

Ähnlich wie Drupal 8 Services And Dependency Injection (20)

Anzeige

Aktuellste (20)

Anzeige

Drupal 8 Services And Dependency Injection

  1. 1. Drupal 8 Services And Dependency Injection. Phil Norton
  2. 2. Phil Norton Technical Lead at Help run NWDUG Blog at #! code (www.hashbangcode.com) @philipnorton42 on Twitter
  3. 3. NWDUGNORTH WEST DRUPAL USER GROUP 2ND TUESDAY / MONTH STREAM ON YOUTUBE 3RD UNCONFERENCE IN NOVEMBER FOLLOW @NWDUG
  4. 4. Drupal::service('thing');
  5. 5. What Are Drupal Services? • Allow access to lots of things in Drupal 8. • Wrap objects and define a common interface. • Automatic Dependency Injection. • Powerful • Certified awesome
  6. 6. config cron renderer path route file_system plugin cache date access_manager event_dispatcher menu translation entity
  7. 7. Usage.
  8. 8. $object = Drupal::service('thing'); $object->method();
  9. 9. $pathManager = Drupal::service('path.alias_manager'); $path = 'somepath'; $normalPath = $pathManager->getPathByAlias($path);
  10. 10. $pathManager = Drupal::service('path.alias_manager'); $path = 'somepath'; $normalPath = $pathManager->getPathByAlias($path); Instantiated AliasManager object
  11. 11. $normalPath = Drupal::service('path.alias_manager') ->getPathByAlias('somepath');
  12. 12. /* @var DrupalCorePathAliasManager $pathManager */ $pathManager = Drupal::service('path.alias_manager');
  13. 13. Where To Find Services.
  14. 14. Services are defined in yml files. <module>.services.yml
  15. 15. core.services.yml path.alias_whitelist: class: DrupalCorePathAliasWhitelist tags: - { name: needs_destruction } arguments: [path_alias_whitelist, '@cache.bootstrap', '@lock', '@state', '@path.alias_storage'] path.alias_manager: class: DrupalCorePathAliasManager arguments: ['@path.alias_storage', '@path.alias_whitelist', '@language_manager', '@cache.data'] path.current: class: DrupalCorePathCurrentPathStack arguments: ['@request_stack']
  16. 16. /**
 * The default alias manager implementation.
 */
 class AliasManager implements AliasManagerInterface, CacheDecoratorInterface {
 
  /**
   * Constructs an AliasManager.
   *
   * @param DrupalCorePathAliasStorageInterface $storage
   *   The alias storage service.
   * @param DrupalCorePathAliasWhitelistInterface $whitelist
   *   The whitelist implementation to use.
   * @param DrupalCoreLanguageLanguageManagerInterface $language_manager
   *   The language manager.
   * @param DrupalCoreCacheCacheBackendInterface $cache
   *   Cache backend.
   */
  public function __construct(AliasStorageInterface $storage, AliasWhitelistInterface $whitelist, LanguageManagerInterface $language_manager, CacheBackendInterface $cache) {
    $this->storage = $storage;
    $this->languageManager = $language_manager;
    $this->whitelist = $whitelist;
    $this->cache = $cache;
  }
  17. 17. path.alias_manager: class: DrupalCorePathAliasManager arguments: [
 '@path.alias_storage', 
 '@path.alias_whitelist', 
 
 '@language_manager', 
 
 '@cache.data'
 
 ] public function __construct( AliasStorageInterface $storage, AliasWhitelistInterface $whitelist, LanguageManagerInterface $language_manager, CacheBackendInterface $cache ) {
  18. 18. Service Argument Types. '@config.factory' == Another service. '%container.modules%' == A configuration item. 'config' or true == A variable.
  19. 19. /**
 * The default alias manager implementation.
 */
 class AliasManager implements AliasManagerInterface, CacheDecoratorInterface {
 
  /**
   * Constructs an AliasManager.
   *
   * @param DrupalCorePathAliasStorageInterface $storage
   *   The alias storage service.
   * @param DrupalCorePathAliasWhitelistInterface $whitelist
   *   The whitelist implementation to use.
   * @param DrupalCoreLanguageLanguageManagerInterface $language_manager
   *   The language manager.
   * @param DrupalCoreCacheCacheBackendInterface $cache
   *   Cache backend.
   */
  public function __construct(AliasStorageInterface $storage, AliasWhitelistInterface $whitelist, LanguageManagerInterface $language_manager, CacheBackendInterface $cache) {
    $this->storage = $storage;
    $this->languageManager = $language_manager;
    $this->whitelist = $whitelist;
    $this->cache = $cache;
  }
  20. 20. Dependency Injection.
  21. 21. Dependency Injection. • Instantiate the object you want without having to worry about dependencies. • Symfony DependencyInjection Component manages these dependencies.
  22. 22. path.alias_manager
 Without Using Dependency Injection.
  23. 23. use DrupalCorePathAliasManager; $pathManager = new AliasManager($storage, $whitelist, $language_manager, $cache);
  24. 24. use DrupalCorePathAliasManager; use DrupalCoreAliasStorage; $storage = new AliasStorage($database, $moduleHandler); $pathManager = new AliasManager($storage, $whitelist, $languageManager, $cache);
  25. 25. use DrupalCorePathAliasManager; use DrupalCoreAliasStorage; use DrupalCoreDatabaseDatabase; use DrupalCoreExtensionModuleHandler; $database = Database::getConnection(); $moduleHandler = new ModuleHandler($root, $moduleList, $cacheBackend); $storage = new AliasStorage($database, $moduleHandler); $pathManager = new AliasManager($storage, $whitelist, $languageManager, $cache);
  26. 26. use DrupalCorePathAliasManager; use DrupalCoreAliasStorage; use DrupalCoreDatabaseDatabase; use DrupalCoreExtensionModuleHandler; use DrupalCoreCacheCacheBackendInterface; use DrupalCoreAppRootFactory; use DrupalCoreCacheDatabaseBackend; $database = Database::getConnection(); $root = new AppRootFactory($drupalKernel); $moduleList = Drupal::getContainer() ->getParameter('container.modules'); $cacheBackend = new DatabaseBackend($connection, $checksumProvider, $bin); $moduleHandler = new ModuleHandler($root, $moduleList, $cacheBackend); $storage = new AliasStorage($database, $moduleHandler); $pathManager = new AliasManager($storage, $whitelist, $languageManager, $cache);
  27. 27. use DrupalCorePathAliasManager; use DrupalCoreAliasStorage; use DrupalCoreDatabaseDatabase; use DrupalCoreExtensionModuleHandler; use DrupalCoreCacheCacheBackendInterface; use DrupalCoreAppRootFactory; use DrupalCoreCacheDatabaseBackend; $database = Database::getConnection(); $drupalKernel = ??? $connection = $database; $checksumProvider = ??? $bin = ??? $root = new AppRootFactory($drupalKernel); $moduleList = Drupal::getContainer() ->getParameter('container.modules'); $cacheBackend = new DatabaseBackend($connection, $checksumProvider, $bin); $moduleHandler = new ModuleHandler($root, $moduleList, $cacheBackend); $storage = new AliasStorage($database, $moduleHandler); $pathManager = new AliasManager($storage, $whitelist, $languageManager, $cache);
  28. 28. $pathManager = Drupal::service('path.alias_manager');
  29. 29. Dependency Injection Interface. • Controllers and Forms implement the interface: DrupalCoreDependencyInjection
 ContainerInjectionInterface • Which means they can be injected with Drupal services. • A static create() method defines the services you need in the controller or form.
  30. 30. class ExampleController extends ControllerBase { protected $configFactory; protected $pathAliasManager; public static function create(ContainerInterface $container) { return new static( $container->get('config.factory'), $container->get('path.alias_manager') ); } public function __construct(ConfigFactoryInterface $configFactory, AliasManager $pathAliasManager) { $this->configFactory = $configFactory; $this->pathAliasManager = $pathAliasManager; } }
  31. 31. public function someAction() { ... $normalPath = Drupal::service('path.alias_manager') ->getPathByAlias('somepath'); ... }
  32. 32. public function someAction() { ... $normalPath = Drupal::service('path.alias_manager') ->getPathByAlias('somepath'); $normalPath = $this->pathAliasManager ->getPathByAlias('somepath'); ... }
  33. 33. Quick Recap. • Services and their dependencies are defined in *.services.yml files. • Dependency injection makes life easier. • Controllers and Forms can have dependencies injected into them. • So how do we build our own?
  34. 34. Create Your Own.
  35. 35. <module>.services.yml Create a services definition file.
  36. 36. <module>.services.yml services: <service name>: class: Drupal<full namespace of class> arguments: ['@config.factory', ...]
  37. 37. Service Argument Types. '@config.factory' == Another service. '%container.modules%' == A configuration item. 'config' or true == A variable.
  38. 38. my_module.services.yml services: my_module.myservice: class: Drupalmy_moduleServiceNameModuleService arguments: ['@config.factory']
  39. 39. Create Service Interface. namespace Drupalmy_moduleServiceName; use DrupalCoreConfigConfigFactoryInterface; interface ModuleServiceInterface { public function doSomething(); }
  40. 40. Create Service Class. namespace Drupalmy_moduleServiceName; use DrupalCoreConfigConfigFactoryInterface; class ModuleService implements ModuleServiceInterface { protected $moduleConfigThing; public function __construct(ConfigFactoryInterface $config_factory) { $this->moduleConfigThing = $config_factory ->get('module.config') ->get('config_thing'); } public function doSomething() { } }
  41. 41. Start Using It. Drupal::service('my_module.myservice')->doSomething();
  42. 42. PCA Predict. A real example of Drupal services in action
  43. 43. PCA Predict Module. • Used for address matching and auto complete in forms. • Integrate with PCA Predict web services. • Drupal service created to wrap the web service.
  44. 44. pcapredict.services.yml services: pcapredict: class: Drupalpcapredict_integrationPcaPredictPcaPredict arguments: ['@config.factory']
  45. 45. namespace Drupalpcapredict_integrationPcaPredict; use DrupalCoreConfigConfigFactoryInterface; use Drupalpcapredict_integrationPcaPredictPcaPredictInterface; interface PcaPredictInterface { public function find(array $values, $type = ‘json'); public function retrieve(array $values, $type = ‘json'); }
  46. 46. namespace Drupalpcapredict_integrationPcaPredict; use DrupalCoreConfigConfigFactoryInterface; use Drupalpcapredict_integrationPcaPredictPcaPredictInterface; class PcaPredict implements PcaPredictInterface { protected $pcaPredictKey; public function __construct(ConfigFactoryInterface $config_factory) { $this->pcaPredictKey = $config_factory ->get('pcapredict.settings') ->get('pcapredict_apikey'); } public function find(array $values, $type = 'json') { // Load data from the API. } public function retrieve(array $values, $type = 'json') { // Load data from the API. } }
  47. 47. namespace Drupalpcapredict_integrationPcaPredict; use DrupalCoreConfigConfigFactoryInterface; use Drupalpcapredict_integrationPcaPredictPcaPredictInterface; class PcaPredict implements PcaPredictInterface { protected $pcaPredictKey; public function __construct(ConfigFactoryInterface $config_factory) { $this->pcaPredictKey = $config_factory ->get('pcapredict.settings') ->get('pcapredict_apikey'); } public function find(array $values, $type = 'json') { // Load data from the API. } public function retrieve(array $values, $type = 'json') { // Load data from the API. } } Implements our interface Store the API key in a property
  48. 48. $results = Drupal::service('pcapredict') ->find(['postcode' => 'M32 0RS']);
  49. 49. Altering Services.
  50. 50. Altering Services. • All services can be overridden or altered. • A service provider class allows this. • These are automatically detected by Drupal. • Camel case version of the module name. Suffixed by the words “ServiceProvider” in the src directory. • Some examples of this in action.
  51. 51. Problem: Shield module prevents testing of API service on staging website. Solution: Poke a hole in the shield!
  52. 52. Module shield_override. • Convert module name to camel case. • shield_override becomes ShieldOverride. • Suffixed by the words “ServiceProvider”.
 
 ShieldOverrideServiceProvider
  53. 53. Module shield_override. shield_override.info.yml src/ShieldOverrideServiceProvider.php src/ShieldOverride.php
  54. 54. namespace Drupalshield_override; use DrupalCoreDependencyInjectionContainerBuilder; use DrupalCoreDependencyInjectionServiceProviderBase; class ShieldOverrideServiceProvider extends ServiceProviderBase { public function alter(ContainerBuilder $container) { // Decorate the shield module to prevent it from // triggering on certain routes. $definition = $container->getDefinition('shield.middleware'); $definition->setClass('Drupalshield_overrideShieldOverride'); } } ShieldOverrideServiceProvider.php
  55. 55. namespace Drupalshield_override; use DrupalshieldShieldMiddleware; use SymfonyComponentHttpFoundationRequest; class ShieldOverride extends ShieldMiddleware { public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) { // Get the current request URI. $currentPath = $request->getRequestUri(); // Get the current method (e.g. GET or POST). $currentMethod = $request->getMethod(); if (($currentMethod == 'POST' || $currentMethod == 'GET') && strstr($currentPath, '/the/soap/service/path') !== FALSE) { // If we are attempting to access the soap service via // a POST or GET HTTP method then handle the request // without invoking the Shield module. return $this->httpKernel->handle($request, $type, $catch); } // Always handle the request using the default // Shield behaviour. return parent::handle($request, $type, $catch); } }
  56. 56. namespace Drupalshield_override; use DrupalshieldShieldMiddleware; use SymfonyComponentHttpFoundationRequest; class ShieldOverride extends ShieldMiddleware { public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) { // Get the current request URI. $currentPath = $request->getRequestUri(); // Get the current method (e.g. GET or POST). $currentMethod = $request->getMethod(); if (($currentMethod == 'POST' || $currentMethod == 'GET') && strstr($currentPath, '/the/soap/service/path') !== FALSE) { // If we are attempting to access the soap service via // a POST or GET HTTP method then handle the request // without invoking the Shield module. return $this->httpKernel->handle($request, $type, $catch); } // Always handle the request using the default // Shield behaviour. return parent::handle($request, $type, $catch); } } Extends the original Shield module class Runs the original Shield code.
  57. 57. Problem: Testing PCA Predict needs an API account and costs money per transaction. Solution: Create a ‘stub’ service override that doesn’t use the API.
  58. 58. Module pcapredict_stub. • Convert module name to camel case. • pcapredict_stub becomes PcapredictStub. • Suffixed by the words “ServiceProvider”.
 
 PcapredictStubServiceProvider
  59. 59. PCA Predict Stub Module. pcapredict_stub.info.yml src/PcapredictStubServiceProvider.php src/PcaPredict/PcaPredictStub.php
  60. 60. namespace Drupalpcapredict_stub; use DrupalCoreDependencyInjectionContainerBuilder; use DrupalCoreDependencyInjectionServiceProviderBase; class PcapredictStubServiceProvider extends ServiceProviderBase { public function alter(ContainerBuilder $container) { // Override the PcaPredict class with a new class. $definition = $container->getDefinition('pcapredict'); $definition->setClass('Drupalpcapredict_stubPcaPredictPcaPredictStub'); } } PcapredictStubServiceProvider.php
  61. 61. namespace Drupalpcapredict_stubPcaPredict; use DrupalpcapredictPcaPredictPcaPredictInterface; use DrupalCoreConfigConfigFactoryInterface; class PcaPredictStub implements PcaPredictInterface { protected $pcaPredictKey; public function __construct(ConfigFactoryInterface $config_factory) { $this->pcaPredictKey = $config_factory ->get('pcapredict.settings') ->get('pcapredict_apikey'); } public function find(array $values, $type = 'json') { // Load data from a CSV file. } public function retrieve(array $values, $type = 'json') { // Load data from a CSV file. } }
  62. 62. namespace Drupalpcapredict_stubPcaPredict; use DrupalpcapredictPcaPredictPcaPredictInterface; use DrupalCoreConfigConfigFactoryInterface; class PcaPredictStub implements PcaPredictInterface { protected $pcaPredictKey; public function __construct(ConfigFactoryInterface $config_factory) { $this->pcaPredictKey = $config_factory ->get('pcapredict.settings') ->get('pcapredict_apikey'); } public function find(array $values, $type = 'json') { // Load data from a CSV file. } public function retrieve(array $values, $type = 'json') { // Load data from a CSV file. } } Implements PcaPredictInterface
  63. 63. find() / retrieve()API Address finder
  64. 64. find() / retrieve() Address finder
  65. 65. Stub Modules. • Stub modules are useful for: • Prevent analytics being sent. • Bypass complex setups / firewalls. • Testing without using the API.
  66. 66. Problem: Filtering a AJAX View with Group context results in access error. Solution: Create our own access rule.
  67. 67. Group View. • Group module used to manage members. • Created a View to show all group members and some information about them. • Added AJAX filters to allow the user list to be filtered. • Group context was loaded into the View using a contextual filter and the path: 
 /account/%group/members
  68. 68. https://www.example.com/account/123/members
  69. 69. https://www.example.com/account/123/members https://www.example.com/views/ajax?_wrapper_format=drupal_ajax (+ POST data)
  70. 70. namespace DrupalgroupContext; use DrupalCoreCacheCacheableMetadata; use DrupalCorePluginContextContext; use DrupalCorePluginContextContextDefinition; use DrupalCorePluginContextContextProviderInterface; use DrupalCoreRoutingRouteMatchInterface; use DrupalCoreStringTranslationStringTranslationTrait; /**  * Sets the current group as a context on group routes.  */ class GroupRouteContext implements ContextProviderInterface { use GroupRouteContextTrait; // ... snip ... /**    * {@inheritdoc}    */ public function getRuntimeContexts(array $unqualified_context_ids) { // Create an optional context definition for group entities. $context_definition = new ContextDefinition('entity:group', NULL, FALSE); // Cache this context on the route. $cacheability = new CacheableMetadata(); $cacheability->setCacheContexts(['route']); // Create a context from the definition and retrieved or created group. $context = new Context($context_definition, $this->getGroupFromRoute()); $context->addCacheableDependency($cacheability); return ['group' => $context]; } // ... snip ... }
  71. 71. namespace DrupalgroupContext; use DrupalCoreCacheCacheableMetadata; use DrupalCorePluginContextContext; use DrupalCorePluginContextContextDefinition; use DrupalCorePluginContextContextProviderInterface; use DrupalCoreRoutingRouteMatchInterface; use DrupalCoreStringTranslationStringTranslationTrait; /**  * Sets the current group as a context on group routes.  */ class GroupRouteContext implements ContextProviderInterface { use GroupRouteContextTrait; // ... snip ... /**    * {@inheritdoc}    */ public function getRuntimeContexts(array $unqualified_context_ids) { // Create an optional context definition for group entities. $context_definition = new ContextDefinition('entity:group', NULL, FALSE); // Cache this context on the route. $cacheability = new CacheableMetadata(); $cacheability->setCacheContexts(['route']); // Create a context from the definition and retrieved or created group. $context = new Context($context_definition, $this->getGroupFromRoute()); $context->addCacheableDependency($cacheability); return ['group' => $context]; } // ... snip ... } No Group in route when in AJAX mode
  72. 72. group.services.yml group.group_route_context:
 class: 'DrupalgroupContextGroupRouteContext'
 arguments: ['@current_route_match']
 tags:
 - { name: 'context_provider' }
  73. 73. GroupViewsServiceProvider.php namespace Drupalgroup_views; use DrupalCoreDependencyInjectionContainerBuilder; use DrupalCoreDependencyInjectionServiceProviderBase; class GroupViewsServiceProvider extends ServiceProviderBase { public function alter(ContainerBuilder $container) { // Decorate the group_route_context service to inject our own objects on // certain routes. $definition = $container->getDefinition('group.group_route_context'); $definition->setClass('Drupalgroup_subscriptionsGroupViewsAccessOverride'); } }
  74. 74. namespace Drupalgroup_views; use DrupalgroupContextGroupRouteContext; use DrupalCorePluginContextContextDefinition; use DrupalCoreCacheCacheableMetadata; use DrupalCorePluginContextContext; class GroupViewsAccessOverride extends GroupRouteContext { public function getRuntimeContexts(array $unqualified_context_ids) { $request = Drupal::request(); // Get the current request URI. $currentPathInfo = $request->getPathInfo(); // Get the current method (e.g. GET or POST). $currentMethod = $request->getMethod(); // Extract the parameters out of the post arguments. parse_str($request->getContent(), $postArgs); if ($currentMethod == 'POST' && $currentPathInfo == '/views/ajax' && isset($postArgs['view_name']) && isset($postArgs['view_args']) && isset($postArgs['_drupal_ajax']) && $postArgs['view_name'] == 'group_members' && is_numeric($postArgs['view_args']) && $postArgs['_drupal_ajax'] == '1' ) { // This is our view. $context_definition = new ContextDefinition('entity:group', NULL, FALSE); // Cache this context on the route. $cacheability = new CacheableMetadata(); $cacheability->setCacheContexts(['route']); // Create a context from the definition and retrieved or created group. $groupEntity = Drupal::service('entity_type.manager')->getStorage('group')->load($postArgs['view_args']); if ($groupEntity) { // We have loaded a group. $context = new Context($context_definition, $groupEntity); $context->addCacheableDependency($cacheability); return ['group' => $context]; } } // Always handle the request using the default GroupRouteContext behaviour. return parent::getRuntimeContexts($unqualified_context_ids); } }
  75. 75. namespace Drupalgroup_views; use DrupalgroupContextGroupRouteContext; use DrupalCorePluginContextContextDefinition; use DrupalCoreCacheCacheableMetadata; use DrupalCorePluginContextContext; class GroupViewsAccessOverride extends GroupRouteContext { public function getRuntimeContexts(array $unqualified_context_ids) { $request = Drupal::request(); // Get the current request URI. $currentPathInfo = $request->getPathInfo(); // Get the current method (e.g. GET or POST). $currentMethod = $request->getMethod(); // Extract the parameters out of the post arguments. parse_str($request->getContent(), $postArgs); if ($currentMethod == 'POST' && $currentPathInfo == '/views/ajax' && isset($postArgs['view_name']) && isset($postArgs['view_args']) && isset($postArgs['_drupal_ajax']) && $postArgs['view_name'] == 'group_members' && is_numeric($postArgs['view_args']) && $postArgs['_drupal_ajax'] == '1' ) { // This is our view. $context_definition = new ContextDefinition('entity:group', NULL, FALSE); // Cache this context on the route. $cacheability = new CacheableMetadata(); $cacheability->setCacheContexts(['route']); // Create a context from the definition and retrieved or created group. $groupEntity = Drupal::service('entity_type.manager')->getStorage('group')->load($postArgs['view_args']); if ($groupEntity) { // We have loaded a group. $context = new Context($context_definition, $groupEntity); $context->addCacheableDependency($cacheability); return ['group' => $context]; } } // Always handle the request using the default GroupRouteContext behaviour. return parent::getRuntimeContexts($unqualified_context_ids); } } Extends the original Group module class Checking to ensure that this is the correct context. Continue on as normal. Load Group object and add it to a context.
  76. 76. Resources. • Services and Dependency Injection Container
 https://api.drupal.org/api/drupal/core!core.api.php/ group/container/ • List of all services
 https://api.drupal.org/api/drupal/services • Services And Dependency Injection
 https://www.drupal.org/docs/8/api/services-and- dependency-injection
  77. 77. NWDUGNORTH WEST DRUPAL USER GROUP 2ND TUESDAY / MONTH STREAM ON YOUTUBE 3RD UNCONFERENCE IN NOVEMBER FOLLOW @NWDUG
  78. 78. Thank you.
  79. 79. Phil Norton Technical Lead at Help run NWDUG Blog at #! code (www.hashbangcode.com) @philipnorton42 on Twitter

×