diff --git a/core/core.services.yml b/core/core.services.yml index e39252d..6961c92 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -438,8 +438,9 @@ services: theme.negotiator: class: Drupal\Core\Theme\ThemeNegotiator arguments: ['@access_check.theme'] + parent: container.trait tags: - - { name: service_collector, tag: theme_negotiator, call: addNegotiator } + - { name: service_id_collector, tag: theme_negotiator } theme.negotiator.default: class: Drupal\Core\Theme\DefaultNegotiator arguments: ['@config.factory'] diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php index aad1dcd..05e106f 100644 --- a/core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php @@ -15,24 +15,28 @@ /** * Collects services to add/inject them into a consumer service. * - * This mechanism allows a service to get multiple processor services injected, - * in order to establish an extensible architecture. + * This mechanism allows a service to get multiple processor services or just + * their IDs injected, in order to establish an extensible architecture. * - * It differs from the factory pattern in that processors are not lazily - * instantiated on demand; the consuming service receives instances of all - * registered processors when it is instantiated. Unlike a factory service, the - * consuming service is not ContainerAware. + * The service collector differs from the factory pattern in that processors are + * not lazily instantiated on demand; the consuming service receives instances + * of all registered processors when it is instantiated. Unlike a factory + * service, the consuming service is not ContainerAware. It differs from regular + * service definition arguments (constructor injection) in that a consuming + * service MAY allow further processors to be added dynamically at runtime. This + * is why the called method (optionally) receives the priority of a processor as + * second argument. * - * It differs from plugins in that all processors are explicitly registered by + * To lazily instantiate services the service ID collector pattern can be used, + * but the consumer service needs to be ContainerAware. As constructor injection + * is used, processors cannot be added at runtime via this method. However, a + * consuming service could have setter methods to allow runtime additions. + * + * These differ from plugins in that all processors are explicitly registered by * service providers (driven by declarative configuration in code); the mere * availability of a processor (cf. plugin discovery) does not imply that a * processor ought to be registered and used. * - * It differs from regular service definition arguments (constructor injection) - * in that a consuming service MAY allow further processors to be added - * dynamically at runtime. This is why the called method (optionally) receives - * the priority of a processor as second argument. - * * @see \Drupal\Core\DependencyInjection\Compiler\TaggedHandlersPass::process() */ class TaggedHandlersPass implements CompilerPassInterface { @@ -40,26 +44,34 @@ class TaggedHandlersPass implements CompilerPassInterface { /** * {@inheritdoc} * - * Finds services tagged with 'service_collector', then finds all - * corresponding tagged services and adds a method call for each to the + * Finds services tagged with 'service_collector' or 'service_id_collector', + * then finds all corresponding tagged services. + * + * The service collector adds a method call for each to the * consuming/collecting service definition. * - * Supported 'service_collector' tag attributes: + * The service ID collector will collect an array of service IDs and add them + * as a constructor argument. + * + * Supported tag attributes: * - tag: The tag name used by handler services to collect. Defaults to the * service ID of the consumer. - * - call: The method name to call on the consumer service. Defaults to - * 'addHandler'. The called method receives two arguments: - * - The handler instance as first argument. - * - Optionally the handler's priority as second argument, if the method - * accepts a second parameter and its name is "priority". In any case, all - * handlers registered at compile time are sorted already. * - required: Boolean indicating if at least one handler service is required. * Defaults to FALSE. * + * Additional tag attributes supported by 'service_collector' only: + * - call: The method name to call on the consumer service. Defaults to + * 'addHandler'. The called method receives two arguments: + * - The handler instance as first argument. + * - Optionally the handler's priority as second argument, if the method + * accepts a second parameter and its name is "priority". In any case, + * all handlers registered at compile time are sorted already. + * * Example (YAML): * @code * tags: * - { name: service_collector, tag: breadcrumb_builder, call: addBuilder } + * - { name: service_id_collector, tag: theme_negotiator } * @endcode * * Supported handler tag attributes: @@ -82,90 +94,141 @@ class TaggedHandlersPass implements CompilerPassInterface { public function process(ContainerBuilder $container) { foreach ($container->findTaggedServiceIds('service_collector') as $consumer_id => $passes) { foreach ($passes as $pass) { - $tag = isset($pass['tag']) ? $pass['tag'] : $consumer_id; - $method_name = isset($pass['call']) ? $pass['call'] : 'addHandler'; - $required = isset($pass['required']) ? $pass['required'] : FALSE; - - // Determine parameters. - $consumer = $container->getDefinition($consumer_id); - $method = new \ReflectionMethod($consumer->getClass(), $method_name); - $params = $method->getParameters(); - - $interface_pos = 0; - $id_pos = NULL; - $priority_pos = NULL; - $extra_params = []; - foreach ($params as $pos => $param) { - if ($param->getClass()) { - $interface = $param->getClass(); - } - else if ($param->getName() === 'id') { - $id_pos = $pos; - } - else if ($param->getName() === 'priority') { - $priority_pos = $pos; - } - else { - $extra_params[$param->getName()] = $pos; - } - } - // Determine the ID. - - if (!isset($interface)) { - throw new LogicException(vsprintf("Service consumer '%s' class method %s::%s() has to type-hint an interface.", array( - $consumer_id, - $consumer->getClass(), - $method_name, - ))); - } - $interface = $interface->getName(); - - // Find all tagged handlers. - $handlers = array(); - $extra_arguments = array(); - foreach ($container->findTaggedServiceIds($tag) as $id => $attributes) { - // Validate the interface. - $handler = $container->getDefinition($id); - if (!is_subclass_of($handler->getClass(), $interface)) { - throw new LogicException("Service '$id' for consumer '$consumer_id' does not implement $interface."); - } - $handlers[$id] = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0; - // Keep track of other tagged handlers arguments. - foreach ($extra_params as $name => $pos) { - $extra_arguments[$id][$pos] = isset($attributes[0][$name]) ? $attributes[0][$name] : $params[$pos]->getDefaultValue(); - } - } - if (empty($handlers)) { - if ($required) { - throw new LogicException(sprintf("At least one service tagged with '%s' is required.", $tag)); - } - continue; - } - // Sort all handlers by priority. - arsort($handlers, SORT_NUMERIC); - - // Add a method call for each handler to the consumer service - // definition. - foreach ($handlers as $id => $priority) { - $arguments = array(); - $arguments[$interface_pos] = new Reference($id); - if (isset($priority_pos)) { - $arguments[$priority_pos] = $priority; - } - if (isset($id_pos)) { - $arguments[$id_pos] = $id; - } - // Add in extra arguments. - if (isset($extra_arguments[$id])) { - // Place extra arguments in their right positions. - $arguments += $extra_arguments[$id]; - } - // Sort the arguments by position. - ksort($arguments); - $consumer->addMethodCall($method_name, $arguments); - } + $this->processServiceCollectorPass($pass, $consumer_id, $container); + } + } + + foreach ($container->findTaggedServiceIds('service_id_collector') as $consumer_id => $passes) { + foreach ($passes as $pass) { + $this->processServiceIdCollectorPass($pass, $consumer_id, $container); + } + } + } + + /** + * Processes a service collector service pass. + * + * @param array $pass + * The service collector pass data. + * @param string $consumer_id + * The consumer service ID. + * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container + * The service container. + */ + protected function processServiceCollectorPass(array $pass, $consumer_id, ContainerBuilder $container) { + $tag = isset($pass['tag']) ? $pass['tag'] : $consumer_id; + $method_name = isset($pass['call']) ? $pass['call'] : 'addHandler'; + $required = isset($pass['required']) ? $pass['required'] : FALSE; + + // Determine parameters. + $consumer = $container->getDefinition($consumer_id); + $method = new \ReflectionMethod($consumer->getClass(), $method_name); + $params = $method->getParameters(); + + $interface_pos = 0; + $id_pos = NULL; + $priority_pos = NULL; + $extra_params = []; + foreach ($params as $pos => $param) { + if ($param->getClass()) { + $interface = $param->getClass(); + } + else if ($param->getName() === 'id') { + $id_pos = $pos; + } + else if ($param->getName() === 'priority') { + $priority_pos = $pos; + } + else { + $extra_params[$param->getName()] = $pos; } } + // Determine the ID. + + if (!isset($interface)) { + throw new LogicException(vsprintf("Service consumer '%s' class method %s::%s() has to type-hint an interface.", array( + $consumer_id, + $consumer->getClass(), + $method_name, + ))); + } + $interface = $interface->getName(); + + // Find all tagged handlers. + $handlers = array(); + $extra_arguments = array(); + foreach ($container->findTaggedServiceIds($tag) as $id => $attributes) { + // Validate the interface. + $handler = $container->getDefinition($id); + if (!is_subclass_of($handler->getClass(), $interface)) { + throw new LogicException("Service '$id' for consumer '$consumer_id' does not implement $interface."); + } + $handlers[$id] = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0; + // Keep track of other tagged handlers arguments. + foreach ($extra_params as $name => $pos) { + $extra_arguments[$id][$pos] = isset($attributes[0][$name]) ? $attributes[0][$name] : $params[$pos]->getDefaultValue(); + } + } + + if ($required && empty($handlers)) { + throw new LogicException(sprintf("At least one service tagged with '%s' is required.", $tag)); + } + + // Sort all handlers by priority. + arsort($handlers, SORT_NUMERIC); + + // Add a method call for each handler to the consumer service + // definition. + foreach ($handlers as $id => $priority) { + $arguments = array(); + $arguments[$interface_pos] = new Reference($id); + if (isset($priority_pos)) { + $arguments[$priority_pos] = $priority; + } + if (isset($id_pos)) { + $arguments[$id_pos] = $id; + } + // Add in extra arguments. + if (isset($extra_arguments[$id])) { + // Place extra arguments in their right positions. + $arguments += $extra_arguments[$id]; + } + // Sort the arguments by position. + ksort($arguments); + $consumer->addMethodCall($method_name, $arguments); + } + } + + /** + * Processes a service collector ID service pass. + * + * @param array $pass + * The service collector pass data. + * @param string $consumer_id + * The consumer service ID. + * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container + * The service container. + */ + protected function processServiceIdCollectorPass(array $pass, $consumer_id, ContainerBuilder $container) { + $tag = isset($pass['tag']) ? $pass['tag'] : $consumer_id; + $required = isset($pass['required']) ? $pass['required'] : FALSE; + + $consumer = $container->getDefinition($consumer_id); + + // Find all tagged handlers. + $handlers = []; + foreach ($container->findTaggedServiceIds($tag) as $id => $attributes) { + $handlers[$id] = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0; + } + + if ($required && empty($handlers)) { + throw new LogicException(sprintf("At least one service tagged with '%s' is required.", $tag)); + } + + // Sort all handlers by priority. + arsort($handlers, SORT_NUMERIC); + + $consumer->addArgument(array_keys($handlers)); } } diff --git a/core/lib/Drupal/Core/Theme/ThemeNegotiator.php b/core/lib/Drupal/Core/Theme/ThemeNegotiator.php index 38c2014..706188a 100644 --- a/core/lib/Drupal/Core/Theme/ThemeNegotiator.php +++ b/core/lib/Drupal/Core/Theme/ThemeNegotiator.php @@ -8,6 +8,7 @@ namespace Drupal\Core\Theme; use Drupal\Core\Routing\RouteMatchInterface; +use Symfony\Component\DependencyInjection\ContainerAware; /** * Provides a class which determines the active theme of the page. @@ -15,23 +16,14 @@ * It therefore uses ThemeNegotiatorInterface objects which are passed in * using the 'theme_negotiator' tag. */ -class ThemeNegotiator implements ThemeNegotiatorInterface { +class ThemeNegotiator extends ContainerAware implements ThemeNegotiatorInterface { /** - * Holds arrays of theme negotiators, keyed by priority. + * Holds arrays of theme negotiators, sorted by priority. * * @var array */ - protected $negotiators = array(); - - /** - * Holds the array of theme negotiators sorted by priority. - * - * Set to NULL if the array needs to be re-calculated. - * - * @var array|NULL - */ - protected $sortedNegotiators; + protected $negotiators = []; /** * The access checker for themes. @@ -46,42 +38,9 @@ class ThemeNegotiator implements ThemeNegotiatorInterface { * @param \Drupal\Core\Theme\ThemeAccessCheck $theme_access * The access checker for themes. */ - public function __construct(ThemeAccessCheck $theme_access) { + public function __construct(ThemeAccessCheck $theme_access, array $negotiators) { $this->themeAccess = $theme_access; - } - - /** - * Adds a active theme negotiation service. - * - * @param \Drupal\Core\Theme\ThemeNegotiatorInterface $negotiator - * The theme negotiator to add. - * @param int $priority - * Priority of the theme negotiator. - */ - public function addNegotiator(ThemeNegotiatorInterface $negotiator, $priority) { - $this->negotiators[$priority][] = $negotiator; - // Force the negotiators to be re-sorted. - $this->sortedNegotiators = NULL; - } - - /** - * Returns the sorted array of theme negotiators. - * - * @return array|\Drupal\Core\Theme\ThemeNegotiatorInterface[] - * An array of theme negotiator objects. - */ - protected function getSortedNegotiators() { - if (!isset($this->sortedNegotiators)) { - // Sort the negotiators according to priority. - krsort($this->negotiators); - // Merge nested negotiators from $this->negotiators into - // $this->sortedNegotiators. - $this->sortedNegotiators = array(); - foreach ($this->negotiators as $builders) { - $this->sortedNegotiators = array_merge($this->sortedNegotiators, $builders); - } - } - return $this->sortedNegotiators; + $this->negotiators = $negotiators; } /** @@ -95,7 +54,9 @@ public function applies(RouteMatchInterface $route_match) { * {@inheritdoc} */ public function determineActiveTheme(RouteMatchInterface $route_match) { - foreach ($this->getSortedNegotiators() as $negotiator) { + foreach ($this->negotiators as $negotiator_id) { + $negotiator = $this->container->get($negotiator_id); + if ($negotiator->applies($route_match)) { $theme = $negotiator->determineActiveTheme($route_match); if ($theme !== NULL && $this->themeAccess->checkAccess($theme)) { diff --git a/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/TaggedHandlersPassTest.php b/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/TaggedHandlersPassTest.php index 055a907..3918c31 100644 --- a/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/TaggedHandlersPassTest.php +++ b/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/TaggedHandlersPassTest.php @@ -61,6 +61,26 @@ public function testProcessRequiredHandlers() { } /** + * Tests a required consumer with no handlers. + * + * @expectedException \Symfony\Component\DependencyInjection\Exception\LogicException + * @expectedExceptionMessage At least one service tagged with 'consumer_id' is required. + * @covers ::process + * @covers ::processServiceIdCollectorPass + */ + public function testIdCollectorProcessRequiredHandlers() { + $container = $this->buildContainer(); + $container + ->register('consumer_id', __NAMESPACE__ . '\ValidConsumer') + ->addTag('service_id_collector', [ + 'required' => TRUE, + ]); + + $handler_pass = new TaggedHandlersPass(); + $handler_pass->process($container); + } + + /** * Tests consumer with missing interface in non-production environment. * * @expectedException \Symfony\Component\DependencyInjection\Exception\LogicException @@ -103,6 +123,32 @@ public function testProcess() { } /** + * Tests one consumer and two handlers with service ID collection. + * + * @covers ::process + */ + public function testserviceIdProcess() { + $container = $this->buildContainer(); + $container + ->register('consumer_id', __NAMESPACE__ . '\ValidConsumer') + ->addTag('service_id_collector'); + + $container + ->register('handler1', __NAMESPACE__ . '\ValidHandler') + ->addTag('consumer_id'); + $container + ->register('handler2', __NAMESPACE__ . '\ValidHandler') + ->addTag('consumer_id'); + + $handler_pass = new TaggedHandlersPass(); + $handler_pass->process($container); + + $arguments = $container->getDefinition('consumer_id')->getArguments(); + $this->assertCount(1, $arguments); + $this->assertCount(2, $arguments[0]); + } + + /** * Tests handler priority sorting. * * @covers ::process @@ -134,6 +180,39 @@ public function testProcessPriority() { } /** + * Tests handler priority sorting for service ID collection. + * + * @covers ::process + */ + public function testserviceIdProcessPriority() { + $container = $this->buildContainer(); + $container + ->register('consumer_id', __NAMESPACE__ . '\ValidConsumer') + ->addTag('service_id_collector'); + + $container + ->register('handler1', __NAMESPACE__ . '\ValidHandler') + ->addTag('consumer_id'); + $container + ->register('handler2', __NAMESPACE__ . '\ValidHandler') + ->addTag('consumer_id', [ + 'priority' => 20, + ]); + $container + ->register('handler3', __NAMESPACE__ . '\ValidHandler') + ->addTag('consumer_id', [ + 'priority' => 10, + ]); + + $handler_pass = new TaggedHandlersPass(); + $handler_pass->process($container); + + $arguments = $container->getDefinition('consumer_id')->getArguments(); + $this->assertCount(1, $arguments); + $this->assertSame(['handler2', 'handler3', 'handler1'], $arguments[0]); + } + + /** * Tests consumer method without priority parameter. * * @covers ::process diff --git a/core/tests/Drupal/Tests/Core/Theme/ThemeNegotiatorTest.php b/core/tests/Drupal/Tests/Core/Theme/ThemeNegotiatorTest.php index 0234ded..ceae0ae 100644 --- a/core/tests/Drupal/Tests/Core/Theme/ThemeNegotiatorTest.php +++ b/core/tests/Drupal/Tests/Core/Theme/ThemeNegotiatorTest.php @@ -7,6 +7,7 @@ namespace Drupal\Tests\Core\Theme; +use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\Routing\RouteMatch; use Drupal\Core\Theme\ThemeNegotiator; use Drupal\Tests\UnitTestCase; @@ -26,6 +27,13 @@ class ThemeNegotiatorTest extends UnitTestCase { protected $themeAccessCheck; /** + * The container builder. + * + * @var \Drupal\Core\DependencyInjection\ContainerBuilder + */ + protected $container; + + /** * The request stack. * * @var \Symfony\Component\HttpFoundation\RequestStack @@ -39,11 +47,14 @@ class ThemeNegotiatorTest extends UnitTestCase { */ protected $themeNegotiator; + /** + * {@inheritdoc} + */ protected function setUp() { $this->themeAccessCheck = $this->getMockBuilder('\Drupal\Core\Theme\ThemeAccessCheck') ->disableOriginalConstructor() ->getMock(); - $this->themeNegotiator = new ThemeNegotiator($this->themeAccessCheck); + $this->container = new ContainerBuilder(); } /** @@ -60,14 +71,16 @@ public function testDetermineActiveTheme() { ->method('applies') ->will($this->returnValue(TRUE)); - $this->themeNegotiator->addNegotiator($negotiator, 0); + $this->container->set('test_negotiator', $negotiator); + + $negotiators = ['test_negotiator']; $this->themeAccessCheck->expects($this->any()) ->method('checkAccess') ->will($this->returnValue(TRUE)); $route_match = new RouteMatch('test_route', new Route('/test-route'), array(), array()); - $theme = $this->themeNegotiator->determineActiveTheme($route_match); + $theme = $this->createThemeNegotiator($negotiators)->determineActiveTheme($route_match); $this->assertEquals('example_test', $theme); } @@ -78,6 +91,8 @@ public function testDetermineActiveTheme() { * @see \Drupal\Core\Theme\ThemeNegotiator::determineActiveTheme() */ public function testDetermineActiveThemeWithPriority() { + $negotiators = []; + $negotiator = $this->getMock('Drupal\Core\Theme\ThemeNegotiatorInterface'); $negotiator->expects($this->once()) ->method('determineActiveTheme') @@ -86,7 +101,7 @@ public function testDetermineActiveThemeWithPriority() { ->method('applies') ->will($this->returnValue(TRUE)); - $this->themeNegotiator->addNegotiator($negotiator, 10); + $negotiators['test_negotiator_1'] = $negotiator; $negotiator = $this->getMock('Drupal\Core\Theme\ThemeNegotiatorInterface'); $negotiator->expects($this->never()) @@ -94,14 +109,18 @@ public function testDetermineActiveThemeWithPriority() { $negotiator->expects($this->never()) ->method('applies'); - $this->themeNegotiator->addNegotiator($negotiator, 0); + $negotiators['test_negotiator_2'] = $negotiator; + + foreach ($negotiators as $id => $negotiator) { + $this->container->set($id, $negotiator); + } $this->themeAccessCheck->expects($this->any()) ->method('checkAccess') ->will($this->returnValue(TRUE)); $route_match = new RouteMatch('test_route', new Route('/test-route'), array(), array()); - $theme = $this->themeNegotiator->determineActiveTheme($route_match); + $theme = $this->createThemeNegotiator(array_keys($negotiators))->determineActiveTheme($route_match); $this->assertEquals('example_test', $theme); } @@ -112,6 +131,8 @@ public function testDetermineActiveThemeWithPriority() { * @see \Drupal\Core\Theme\ThemeNegotiator::determineActiveTheme() */ public function testDetermineActiveThemeWithAccessCheck() { + $negotiators = []; + $negotiator = $this->getMock('Drupal\Core\Theme\ThemeNegotiatorInterface'); $negotiator->expects($this->once()) ->method('determineActiveTheme') @@ -120,7 +141,7 @@ public function testDetermineActiveThemeWithAccessCheck() { ->method('applies') ->will($this->returnValue(TRUE)); - $this->themeNegotiator->addNegotiator($negotiator, 10); + $negotiators['test_negotiator_1'] = $negotiator; $negotiator = $this->getMock('Drupal\Core\Theme\ThemeNegotiatorInterface'); $negotiator->expects($this->once()) @@ -130,7 +151,11 @@ public function testDetermineActiveThemeWithAccessCheck() { ->method('applies') ->will($this->returnValue(TRUE)); - $this->themeNegotiator->addNegotiator($negotiator, 0); + $negotiators['test_negotiator_2'] = $negotiator; + + foreach ($negotiators as $id => $negotiator) { + $this->container->set($id, $negotiator); + } $this->themeAccessCheck->expects($this->at(0)) ->method('checkAccess') @@ -143,7 +168,7 @@ public function testDetermineActiveThemeWithAccessCheck() { ->will($this->returnValue(TRUE)); $route_match = new RouteMatch('test_route', new Route('/test-route'), array(), array()); - $theme = $this->themeNegotiator->determineActiveTheme($route_match); + $theme = $this->createThemeNegotiator(array_keys($negotiators))->determineActiveTheme($route_match); $this->assertEquals('example_test2', $theme); } @@ -154,6 +179,8 @@ public function testDetermineActiveThemeWithAccessCheck() { * @see \Drupal\Core\Theme\ThemeNegotiatorInterface */ public function testDetermineActiveThemeWithNotApplyingNegotiator() { + $negotiators = []; + $negotiator = $this->getMock('Drupal\Core\Theme\ThemeNegotiatorInterface'); $negotiator->expects($this->never()) ->method('determineActiveTheme'); @@ -161,7 +188,7 @@ public function testDetermineActiveThemeWithNotApplyingNegotiator() { ->method('applies') ->will($this->returnValue(FALSE)); - $this->themeNegotiator->addNegotiator($negotiator, 10); + $negotiators['test_negotiator_1'] = $negotiator; $negotiator = $this->getMock('Drupal\Core\Theme\ThemeNegotiatorInterface'); $negotiator->expects($this->once()) @@ -171,16 +198,35 @@ public function testDetermineActiveThemeWithNotApplyingNegotiator() { ->method('applies') ->will($this->returnValue(TRUE)); - $this->themeNegotiator->addNegotiator($negotiator, 0); + $negotiators['test_negotiator_2'] = $negotiator; + + foreach ($negotiators as $id => $negotiator) { + $this->container->set($id, $negotiator); + } $this->themeAccessCheck->expects($this->any()) ->method('checkAccess') ->will($this->returnValue(TRUE)); $route_match = new RouteMatch('test_route', new Route('/test-route'), array(), array()); - $theme = $this->themeNegotiator->determineActiveTheme($route_match); + $theme = $this->createThemeNegotiator(array_keys($negotiators))->determineActiveTheme($route_match); $this->assertEquals('example_test2', $theme); } + /** + * Creates a new theme negotiator instance. + * + * @param array $negotiators + * An array of negotiator IDs. + * + * @return \Drupal\Core\Theme\ThemeNegotiator + */ + protected function createThemeNegotiator(array $negotiators) { + $theme_negotiator = new ThemeNegotiator($this->themeAccessCheck, $negotiators); + $theme_negotiator->setContainer($this->container); + + return $theme_negotiator; + } + }