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
6. 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
23. Dependency Injection.
• Instantiate the object you want without having to
worry about dependencies.
• Symfony DependencyInjection Component
manages these dependencies.
27. 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);
28. 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);
29. 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);
31. 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.
32. 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;
}
}
33. public function someAction() {
...
$normalPath = Drupal::service('path.alias_manager')
->getPathByAlias('somepath');
...
}
34. public function someAction() {
...
$normalPath = Drupal::service('path.alias_manager')
->getPathByAlias('somepath');
$normalPath = $this->pathAliasManager
->getPathByAlias('somepath');
...
}
35. 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?
41. Create Service Interface.
namespace Drupalmy_moduleServiceName;
use DrupalCoreConfigConfigFactoryInterface;
interface ModuleServiceInterface {
public function doSomething();
}
42. 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() {
}
}
45. 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.
48. 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.
}
}
49. 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
52. 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.
54. Module shield_override.
• Convert module name to camel case.
• shield_override becomes ShieldOverride.
• Suffixed by the words “ServiceProvider”.
ShieldOverrideServiceProvider
56. 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
57. 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);
}
}
58. 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.
59. 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.
60. Module pcapredict_stub.
• Convert module name to camel case.
• pcapredict_stub becomes PcapredictStub.
• Suffixed by the words “ServiceProvider”.
PcapredictStubServiceProvider
62. 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
63. 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.
}
}
64. 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
67. Stub Modules.
• Stub modules are useful for:
• Prevent analytics being sent.
• Bypass complex setups / firewalls.
• Testing without using the API.
68. Problem:
Filtering a AJAX View with
Group context results in
access error.
Solution:
Create our own access rule.
69. 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
72. 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 ...
}
73. 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
75. 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');
}
}
76. 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);
}
}
77. 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.
78. 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
79. NWDUGNORTH WEST DRUPAL USER GROUP
2ND TUESDAY / MONTH
STREAM ON YOUTUBE
3RD UNCONFERENCE IN
NOVEMBER
FOLLOW @NWDUG