Issue #2354889 by larowlan, dawehner, lauriii, Berdir, catch, martin107, pfrenssen, EclipseGc, Fabianx, Wim Leers, dsnopek, jibran, tim.plunkett, andypost: Make block context faster by removing onBlock event and replace it with loading from a ContextManager

8.0.x
Alex Pott 2015-07-08 13:01:20 +01:00
parent 46e6f72399
commit 794be16300
30 changed files with 551 additions and 332 deletions

View File

@ -2186,6 +2186,9 @@ function hook_validation_constraint_alter(array &$definitions) {
* at the end of a request to finalize operations, if this service was
* instantiated. Services should implement \Drupal\Core\DestructableInterface
* in this case.
* - context_provider: Indicates a block context provider, used for example
* by block conditions. It has to implement
* \Drupal\Core\Plugin\Context\ContextProviderInterface.
*
* Creating a tag for a service does not do anything on its own, but tags
* can be discovered or queried in a compiler pass when the container is built,

View File

@ -266,6 +266,9 @@ services:
context.handler:
class: Drupal\Core\Plugin\Context\ContextHandler
arguments: ['@typed_data_manager']
context.repository:
class: Drupal\Core\Plugin\Context\LazyContextRepository
arguments: ['@service_container']
cron:
class: Drupal\Core\Cron
arguments: ['@module_handler', '@lock', '@queue', '@state', '@account_switcher', '@logger.channel.cron', '@plugin.manager.queue_worker']

View File

@ -10,6 +10,7 @@ namespace Drupal\Core;
use Drupal\Core\Cache\Context\CacheContextsPass;
use Drupal\Core\Cache\ListCacheBinsPass;
use Drupal\Core\DependencyInjection\Compiler\BackendCompilerPass;
use Drupal\Core\DependencyInjection\Compiler\ContextProvidersPass;
use Drupal\Core\DependencyInjection\Compiler\ProxyServicesPass;
use Drupal\Core\DependencyInjection\Compiler\RegisterLazyRouteEnhancers;
use Drupal\Core\DependencyInjection\Compiler\RegisterLazyRouteFilters;
@ -88,6 +89,7 @@ class CoreServiceProvider implements ServiceProviderInterface {
// Add the compiler pass that will process the tagged services.
$container->addCompilerPass(new ListCacheBinsPass());
$container->addCompilerPass(new CacheContextsPass());
$container->addCompilerPass(new ContextProvidersPass());
// Register plugin managers.
$container->addCompilerPass(new PluginManagerPass());

View File

@ -0,0 +1,33 @@
<?php
/**
* @file
* Contains \Drupal\Core\DependencyInjection\Compiler\ContextProvidersPass.
*/
namespace Drupal\Core\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
/**
* Adds the context provider service IDs to the context manager.
*/
class ContextProvidersPass implements CompilerPassInterface {
/**
* {@inheritdoc}
*
* Passes the service IDs of all context providers to the context repository.
*/
public function process(ContainerBuilder $container) {
$context_providers = [];
foreach (array_keys($container->findTaggedServiceIds('context_provider')) as $id) {
$context_providers[] = $id;
}
$definition = $container->getDefinition('context.repository');
$definition->addArgument($context_providers);
}
}

View File

@ -0,0 +1,82 @@
<?php
/**
* @file
* Contains \Drupal\Core\Plugin\Context\ContextProviderInterface.
*/
namespace Drupal\Core\Plugin\Context;
/**
* Defines an interface for providing plugin contexts.
*
* Implementations only need to deal with unqualified context IDs so they only
* need to be unique in the context of a given service provider.
*
* The fully qualified context ID then includes the service ID:
* @{service_id}:{unqualified_context_id}.
*
* @see \Drupal\Core\Plugin\Context\ContextRepositoryInterface
*/
interface ContextProviderInterface {
/**
* Gets runtime context values for the given context IDs.
*
* For context-aware plugins to function correctly, all of the contexts that
* they require must be populated with values. So this method should set a
* value for each context that it adds. For example:
*
* @code
* // Determine a specific node to pass as context to a block.
* $node = ...
*
* // Set that specific node as the value of the 'node' context.
* $context = new Context(new ContextDefinition('entity:node'));
* $context->setContextValue($node);
* return ['node' => $context];
* @endcode
*
* On the other hand, there are cases, on which providers no longer are
* possible to provide context objects, even without the value, so the caller
* should not expect it.
*
* @param string[] $unqualified_context_ids
* The requested context IDs. The context provider must only return contexts
* for those IDs.
*
* @return \Drupal\Core\Plugin\Context\ContextInterface[]
* The determined available contexts, keyed by the unqualified context_id.
*
* @see \Drupal\Core\Plugin\Context\ContextProviderInterface:getAvailableContexts()
*/
public function getRuntimeContexts(array $unqualified_context_ids);
/**
* Gets all available contexts for the purposes of configuration.
*
* When a context aware plugin is being configured, the configuration UI must
* know which named contexts are potentially available, but does not care
* about the value, since the value can be different for each request, and
* might not be available at all during the configuration UI's request.
*
* For example:
* @code
* // During configuration, there is no specific node to pass as context.
* // However, inform the system that a context named 'node' is
* // available, and provide its definition, so that context aware plugins
* // can be configured to use it. When the plugin, for example a block,
* // needs to evaluate the context, the value of this context will be
* // supplied by getRuntimeContexts().
* $context = new Context(new ContextDefinition('entity:node'));
* return ['node' => $context];
* @endcode
*
* @return \Drupal\Core\Plugin\Context\ContextInterface[]
* All available contexts keyed by the unqualified context ID.
*
* @see \Drupal\Core\Plugin\Context\ContextProviderInterface::getRuntimeContext()
*/
public function getAvailableContexts();
}

View File

@ -0,0 +1,46 @@
<?php
/**
* @file
* Contains \Drupal\Core\Plugin\Context\ContextRepositoryInterface.
*/
namespace Drupal\Core\Plugin\Context;
/**
* Offers a global context repository.
*
* Provides a list of all available contexts, which is mostly useful for
* configuration on forms, as well as a method to get the concrete contexts with
* their values, given a list of fully qualified context IDs.
*
* @see \Drupal\Core\Plugin\Context\ContextProviderInterface
*/
interface ContextRepositoryInterface {
/**
* Gets runtime context values for the given context IDs.
*
* Given that context providers might not return contexts for the given
* context IDs, it is also not guaranteed that the context repository returns
* contexts for all specified IDs.
*
* @param string[] $context_ids
* Fully qualified context IDs, which looks like
* @{service_id}:{unqualified_context_id}, so for example
* node.node_route_context:node.
*
* @return \Drupal\Core\Plugin\Context\ContextInterface[]
* The determined contexts, keyed by the fully qualified context ID.
*/
public function getRuntimeContexts(array $context_ids);
/**
* Gets all available contexts for the purposes of configuration.
*
* @return \Drupal\Core\Plugin\Context\ContextInterface[]
* All available contexts.
*/
public function getAvailableContexts();
}

View File

@ -0,0 +1,112 @@
<?php
/**
* @file
* Contains \Drupal\Core\Plugin\Context\LazyContextRepository.
*/
namespace Drupal\Core\Plugin\Context;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a context repository which uses context provider services.
*/
class LazyContextRepository implements ContextRepositoryInterface {
/**
* The set of available context providers service IDs.
*
* @var string[]
* Context provider service IDs.
*/
protected $contextProviderServiceIDs = [];
/**
* The service container.
*
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
/**
* The statically cached contexts.
*
* @var \Drupal\Core\Plugin\Context\ContextInterface[]
*/
protected $contexts = [];
/**
* Constructs a LazyContextRepository object.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* The current service container.
* @param string[] $context_provider_service_ids
* The set of the available context provider service IDs.
*/
public function __construct(ContainerInterface $container, array $context_provider_service_ids) {
$this->container = $container;
$this->contextProviderServiceIDs = $context_provider_service_ids;
}
/**
* {@inheritdoc}
*/
public function getRuntimeContexts(array $context_ids) {
$contexts = [];
// Create a map of context providers (service IDs) to unqualified context
// IDs.
$context_ids_by_service = [];
foreach ($context_ids as $id) {
if (isset($this->contexts[$id])) {
$contexts[$id] = $this->contexts[$id];
continue;
}
// The IDs have been passed in @{service_id}:{unqualified_context_id}
// format.
// @todo Convert to an assert once https://www.drupal.org/node/2408013 is
// in.
if ($id[0] === '@' && strpos($id, ':') !== FALSE) {
list($service_id, $unqualified_context_id) = explode(':', $id, 2);
// Remove the leading '@'.
$service_id = substr($service_id, 1);
}
else {
throw new \InvalidArgumentException('You must provide the context IDs in the @{service_id}:{unqualified_context_id} format.');
}
$context_ids_by_service[$service_id][] = $unqualified_context_id;
}
// Iterate over all missing context providers (services), gather the
// runtime contexts and assign them as requested.
foreach ($context_ids_by_service as $service_id => $unqualified_context_ids) {
$contexts_by_service = $this->container->get($service_id)->getRuntimeContexts($unqualified_context_ids);
$wanted_contexts = array_intersect_key($contexts_by_service, array_flip($unqualified_context_ids));
foreach ($wanted_contexts as $unqualified_context_id => $context) {
$context_id = '@' . $service_id . ':' . $unqualified_context_id;
$this->contexts[$context_id] = $contexts[$context_id] = $context;
}
}
return $contexts;
}
/**
* {@inheritdoc}
*/
public function getAvailableContexts() {
$contexts = [];
foreach ($this->contextProviderServiceIDs as $service_id) {
$contexts_by_service = $this->container->get($service_id)->getAvailableContexts();
foreach ($contexts_by_service as $unqualified_context_id => $context) {
$context_id = '@' . $service_id . ':' . $unqualified_context_id;
$contexts[$context_id] = $context;
}
}
return $contexts;
}
}

View File

@ -7,21 +7,6 @@ services:
class: Drupal\block\EventSubscriber\BlockPageDisplayVariantSubscriber
tags:
- { name: event_subscriber }
block.current_user_context:
class: Drupal\block\EventSubscriber\CurrentUserContext
arguments: ['@current_user', '@entity.manager']
tags:
- { name: 'event_subscriber' }
block.current_language_context:
class: Drupal\block\EventSubscriber\CurrentLanguageContext
arguments: ['@language_manager']
tags:
- { name: 'event_subscriber' }
block.node_route_context:
class: Drupal\block\EventSubscriber\NodeRouteContext
arguments: ['@current_route_match']
tags:
- { name: 'event_subscriber' }
block.repository:
class: Drupal\block\BlockRepository
arguments: ['@entity.manager', '@theme.manager', '@context.handler']

View File

@ -18,6 +18,7 @@ use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Executable\ExecutableManagerInterface;
use Drupal\Core\Plugin\Context\ContextHandlerInterface;
use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
@ -45,6 +46,13 @@ class BlockAccessControlHandler extends EntityAccessControlHandler implements En
*/
protected $contextHandler;
/**
* The context manager service.
*
* @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface
*/
protected $contextRepository;
/**
* {@inheritdoc}
*/
@ -52,7 +60,8 @@ class BlockAccessControlHandler extends EntityAccessControlHandler implements En
return new static(
$entity_type,
$container->get('plugin.manager.condition'),
$container->get('context.handler')
$container->get('context.handler'),
$container->get('context.repository')
);
}
@ -65,11 +74,14 @@ class BlockAccessControlHandler extends EntityAccessControlHandler implements En
* The ConditionManager for checking visibility of blocks.
* @param \Drupal\Core\Plugin\Context\ContextHandlerInterface $context_handler
* The ContextHandler for applying contexts to conditions properly.
* @param \Drupal\Core\Plugin\Context\ContextRepositoryInterface $context_repository
* The lazy context repository service.
*/
public function __construct(EntityTypeInterface $entity_type, ExecutableManagerInterface $manager, ContextHandlerInterface $context_handler) {
public function __construct(EntityTypeInterface $entity_type, ExecutableManagerInterface $manager, ContextHandlerInterface $context_handler, ContextRepositoryInterface $context_repository ) {
parent::__construct($entity_type);
$this->manager = $manager;
$this->contextHandler = $context_handler;
$this->contextRepository = $context_repository;
}
@ -87,12 +99,12 @@ class BlockAccessControlHandler extends EntityAccessControlHandler implements En
return AccessResult::forbidden()->cacheUntilEntityChanges($entity);
}
else {
$contexts = $entity->getContexts();
$conditions = [];
$missing_context = FALSE;
foreach ($entity->getVisibilityConditions() as $condition_id => $condition) {
if ($condition instanceof ContextAwarePluginInterface) {
try {
$contexts = $this->contextRepository->getRuntimeContexts(array_values($condition->getContextMapping()));
$this->contextHandler->applyContextMapping($condition, $contexts);
}
catch (ContextException $e) {

View File

@ -7,8 +7,6 @@
namespace Drupal\block;
use Drupal\block\Event\BlockContextEvent;
use Drupal\block\Event\BlockEvents;
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityManagerInterface;
@ -18,8 +16,8 @@ use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Provides form for block instance forms.
@ -68,6 +66,13 @@ class BlockForm extends EntityForm {
*/
protected $themeHandler;
/**
* The context repository service.
*
* @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface
*/
protected $contextRepository;
/**
* Constructs a BlockForm object.
*
@ -75,17 +80,17 @@ class BlockForm extends EntityForm {
* The entity manager.
* @param \Drupal\Core\Executable\ExecutableManagerInterface $manager
* The ConditionManager for building the visibility UI.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
* The EventDispatcher for gathering administrative contexts.
* @param \Drupal\Core\Plugin\Context\ContextRepositoryInterface $context_repository
* The lazy context repository service.
* @param \Drupal\Core\Language\LanguageManagerInterface $language
* The language manager.
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler.
*/
public function __construct(EntityManagerInterface $entity_manager, ExecutableManagerInterface $manager, EventDispatcherInterface $dispatcher, LanguageManagerInterface $language, ThemeHandlerInterface $theme_handler) {
public function __construct(EntityManagerInterface $entity_manager, ExecutableManagerInterface $manager, ContextRepositoryInterface $context_repository, LanguageManagerInterface $language, ThemeHandlerInterface $theme_handler) {
$this->storage = $entity_manager->getStorage('block');
$this->manager = $manager;
$this->dispatcher = $dispatcher;
$this->contextRepository = $context_repository;
$this->language = $language;
$this->themeHandler = $theme_handler;
}
@ -97,7 +102,7 @@ class BlockForm extends EntityForm {
return new static(
$container->get('entity.manager'),
$container->get('plugin.manager.condition'),
$container->get('event_dispatcher'),
$container->get('context.repository'),
$container->get('language_manager'),
$container->get('theme_handler')
);
@ -117,7 +122,7 @@ class BlockForm extends EntityForm {
// Store the gathered contexts in the form state for other objects to use
// during form building.
$form_state->setTemporaryValue('gathered_contexts', $this->dispatcher->dispatch(BlockEvents::ADMINISTRATIVE_CONTEXT, new BlockContextEvent())->getContexts());
$form_state->setTemporaryValue('gathered_contexts', $this->contextRepository->getAvailableContexts());
$form['#tree'] = TRUE;
$form['settings'] = $entity->getPlugin()->buildConfigurationForm(array(), $form_state);

View File

@ -95,24 +95,6 @@ interface BlockInterface extends ConfigEntityInterface {
*/
public function setVisibilityConfig($instance_id, array $configuration);
/**
* Get all available contexts.
*
* @return \Drupal\Component\Plugin\Context\ContextInterface[]
* An array of set contexts, keyed by context name.
*/
public function getContexts();
/**
* Set the contexts that are available for use within the block entity.
*
* @param \Drupal\Component\Plugin\Context\ContextInterface[] $contexts
* An array of contexts to set on the block.
*
* @return $this
*/
public function setContexts(array $contexts);
/**
* Returns the weight of this block (used for sorting).
*

View File

@ -50,7 +50,7 @@ class BlockRepository implements BlockRepositoryInterface {
/**
* {@inheritdoc}
*/
public function getVisibleBlocksPerRegion(array $contexts, array &$cacheable_metadata = []) {
public function getVisibleBlocksPerRegion(array &$cacheable_metadata = []) {
$active_theme = $this->themeManager->getActiveTheme();
// Build an array of the region names in the right order.
$empty = array_fill_keys($active_theme->getRegions(), array());
@ -58,7 +58,6 @@ class BlockRepository implements BlockRepositoryInterface {
$full = array();
foreach ($this->blockStorage->loadByProperties(array('theme' => $active_theme->getName())) as $block_id => $block) {
/** @var \Drupal\block\BlockInterface $block */
$block->setContexts($contexts);
$access = $block->access('view', NULL, TRUE);
$region = $block->getRegion();
if (!isset($cacheable_metadata[$region])) {

View File

@ -12,8 +12,6 @@ interface BlockRepositoryInterface {
/**
* Returns an array of regions and their block entities.
*
* @param \Drupal\Component\Plugin\Context\ContextInterface[] $contexts
* An array of contexts to set on the blocks.
* @param \Drupal\Core\Cache\CacheableMetadata[] $cacheable_metadata
* (optional) List of CacheableMetadata objects, keyed by region. This is
* by reference and is used to pass this information back to the caller.
@ -22,6 +20,6 @@ interface BlockRepositoryInterface {
* The array is first keyed by region machine name, with the values
* containing an array keyed by block ID, with block entities as the values.
*/
public function getVisibleBlocksPerRegion(array $contexts, array &$cacheable_metadata = []);
public function getVisibleBlocksPerRegion(array &$cacheable_metadata = []);
}

View File

@ -250,21 +250,6 @@ class Block extends ConfigEntityBase implements BlockInterface, EntityWithPlugin
}
}
/**
* {@inheritdoc}
*/
public function setContexts(array $contexts) {
$this->contexts = $contexts;
return $this;
}
/**
* {@inheritdoc}
*/
public function getContexts() {
return $this->contexts;
}
/**
* {@inheritdoc}
*/

View File

@ -1,53 +0,0 @@
<?php
/**
* @file
* Contains \Drupal\block\Event\BlockContextEvent.
*/
namespace Drupal\block\Event;
use Drupal\Core\Plugin\Context\ContextInterface;
use Symfony\Component\EventDispatcher\Event;
/**
* Event subscribers can add context to be used by the block and its conditions.
*
* @see \Drupal\block\Event\BlockEvents::ACTIVE_CONTEXT
* @see \Drupal\block\Event\BlockEvents::ADMINISTRATIVE_CONTEXT
*/
class BlockContextEvent extends Event {
/**
* The array of available contexts for blocks.
*
* @var array
*/
protected $contexts = [];
/**
* Sets the context object for a given name.
*
* @param string $name
* The name to store the context object under.
* @param \Drupal\Core\Plugin\Context\ContextInterface $context
* The context object to set.
*
* @return $this
*/
public function setContext($name, ContextInterface $context) {
$this->contexts[$name] = $context;
return $this;
}
/**
* Returns the context objects.
*
* @return \Drupal\Component\Plugin\Context\ContextInterface[]
* An array of contexts that have been provided.
*/
public function getContexts() {
return $this->contexts;
}
}

View File

@ -1,55 +0,0 @@
<?php
/**
* @file
* Contains \Drupal\block\Event\BlockEvents.
*/
namespace Drupal\block\Event;
/**
* Defines events for the Block module.
*/
final class BlockEvents {
/**
* Name of the event when gathering condition context for a block plugin.
*
* This event allows you to provide additional context that can be used by
* a condition plugin in order to determine the visibility of a block. The
* event listener method receives a \Drupal\block\Event\BlockContextEvent
* instance. Generally any new context is paired with a new condition plugin
* that interprets the provided context and allows the block system to
* determine whether or not the block should be displayed.
*
* @Event
*
* @see \Drupal\Core\Block\BlockBase::getConditionContexts()
* @see \Drupal\block\Event\BlockContextEvent
* @see \Drupal\block\EventSubscriber\NodeRouteContext::onBlockActiveContext()
* @see \Drupal\Core\Condition\ConditionInterface
*/
const ACTIVE_CONTEXT = 'block.active_context';
/**
* Name of the event when gathering contexts for plugin configuration.
*
* This event allows you to provide information about your context to the
* administration UI without having to provide a value for the context. For
* example, during configuration there is no specific node to pass as context.
* However, we still need to inform the system that a context named 'node' is
* available and provide a definition so that blocks can be configured to use
* it.
*
* The event listener method receives a \Drupal\block\Event\BlockContextEvent
* instance.
*
* @Event
*
* @see \Drupal\block\BlockForm::form()
* @see \Drupal\block\Event\BlockContextEvent
* @see \Drupal\block\EventSubscriber\NodeRouteContext::onBlockAdministrativeContext()
*/
const ADMINISTRATIVE_CONTEXT = 'block.administrative_context';
}

View File

@ -1,75 +0,0 @@
<?php
/**
* @file
* Contains \Drupal\block\EventSubscriber\BlockContextSubscriberBase.
*/
namespace Drupal\block\EventSubscriber;
use Drupal\block\Event\BlockContextEvent;
use Drupal\block\Event\BlockEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Provides a base class for block context subscribers.
*/
abstract class BlockContextSubscriberBase implements EventSubscriberInterface {
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[BlockEvents::ACTIVE_CONTEXT][] = 'onBlockActiveContext';
$events[BlockEvents::ADMINISTRATIVE_CONTEXT][] = 'onBlockAdministrativeContext';
return $events;
}
/**
* Determines the available run-time contexts.
*
* For blocks to render correctly, all of the contexts that they require
* must be populated with values. So this method must set a value for each
* context that it adds. For example:
* @code
* // Determine a specific node to pass as context to blocks.
* $node = ...
*
* // Set that specific node as the value of the 'node' context.
* $context = new Context(new ContextDefinition('entity:node'));
* $context->setContextValue($node);
* $event->setContext('node.node', $context);
* @endcode
*
* @param \Drupal\block\Event\BlockContextEvent $event
* The Event to which to register available contexts.
*/
abstract public function onBlockActiveContext(BlockContextEvent $event);
/**
* Determines the available configuration-time contexts.
*
* When a block is being configured, the configuration UI must know which
* named contexts are potentially available, but does not care about the
* value, since the value can be different for each request, and might not
* be available at all during the configuration UI's request.
*
* For example:
* @code
* // During configuration, there is no specific node to pass as context.
* // However, inform the system that a context named 'node.node' is
* // available, and provide its definition, so that blocks can be
* // configured to use it. When the block is rendered, the value of this
* // context will be supplied by onBlockActiveContext().
* $context = new Context(new ContextDefinition('entity:node'));
* $event->setContext('node.node', $context);
* @endcode
*
* @param \Drupal\block\Event\BlockContextEvent $event
* The Event to which to register available contexts.
*
* @see static::onBlockActiveContext()
*/
abstract public function onBlockAdministrativeContext(BlockContextEvent $event);
}

View File

@ -8,18 +8,14 @@
namespace Drupal\block\Plugin\DisplayVariant;
use Drupal\block\BlockRepositoryInterface;
use Drupal\block\Event\BlockContextEvent;
use Drupal\block\Event\BlockEvents;
use Drupal\Core\Block\MainContentBlockPluginInterface;
use Drupal\Core\Block\MessagesBlockPluginInterface;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Display\PageVariantInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityViewBuilderInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Display\VariantBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Provides a page display variant that decorates the main content with blocks.
@ -80,16 +76,13 @@ class BlockPageVariant extends VariantBase implements PageVariantInterface, Cont
* The block repository.
* @param \Drupal\Core\Entity\EntityViewBuilderInterface $block_view_builder
* The block view builder.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
* The event dispatcher.
* @param string[] $block_list_cache_tags
* The Block entity type list cache tags.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, BlockRepositoryInterface $block_repository, EntityViewBuilderInterface $block_view_builder, EventDispatcherInterface $dispatcher, array $block_list_cache_tags) {
public function __construct(array $configuration, $plugin_id, $plugin_definition, BlockRepositoryInterface $block_repository, EntityViewBuilderInterface $block_view_builder, array $block_list_cache_tags) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->blockRepository = $block_repository;
$this->blockViewBuilder = $block_view_builder;
$this->dispatcher = $dispatcher;
$this->blockListCacheTags = $block_list_cache_tags;
}
@ -103,7 +96,6 @@ class BlockPageVariant extends VariantBase implements PageVariantInterface, Cont
$plugin_definition,
$container->get('block.repository'),
$container->get('entity.manager')->getViewBuilder('block'),
$container->get('event_dispatcher'),
$container->get('entity.manager')->getDefinition('block')->getListCacheTags()
);
}
@ -129,10 +121,9 @@ class BlockPageVariant extends VariantBase implements PageVariantInterface, Cont
'tags' => $this->blockListCacheTags,
],
];
$contexts = $this->getActiveBlockContexts();
// Load all region content assigned via blocks.
$cacheable_metadata_list = [];
foreach ($this->blockRepository->getVisibleBlocksPerRegion($contexts, $cacheable_metadata_list) as $region => $blocks) {
foreach ($this->blockRepository->getVisibleBlocksPerRegion($cacheable_metadata_list) as $region => $blocks) {
/** @var $blocks \Drupal\block\BlockInterface[] */
foreach ($blocks as $key => $block) {
$block_plugin = $block->getPlugin();
@ -188,14 +179,4 @@ class BlockPageVariant extends VariantBase implements PageVariantInterface, Cont
return $build;
}
/**
* Returns an array of context objects to set on the blocks.
*
* @return \Drupal\Component\Plugin\Context\ContextInterface[]
* An array of contexts to set on the blocks.
*/
protected function getActiveBlockContexts() {
return $this->dispatcher->dispatch(BlockEvents::ACTIVE_CONTEXT, new BlockContextEvent())->getContexts();
}
}

View File

@ -90,7 +90,7 @@ class BlockLanguageTest extends WebTestBase {
'langcodes' => array(
'fr' => 'fr',
),
'context_mapping' => ['language' => 'language.language_interface'],
'context_mapping' => ['language' => '@language.current_language_context:language_interface'],
),
),
);
@ -142,7 +142,7 @@ class BlockLanguageTest extends WebTestBase {
// Enable a standard block and set visibility to French only.
$block_id = strtolower($this->randomMachineName(8));
$edit = [
'visibility[language][context_mapping][language]' => 'language.language_interface',
'visibility[language][context_mapping][language]' => '@language.current_language_context:language_interface',
'visibility[language][langcodes][fr]' => TRUE,
'id' => $block_id,
'region' => 'sidebar_first',
@ -168,7 +168,7 @@ class BlockLanguageTest extends WebTestBase {
// Change visibility to now depend on content language for this block.
$edit = [
'visibility[language][context_mapping][language]' => 'language.language_content'
'visibility[language][context_mapping][language]' => '@language.current_language_context:language_content'
];
$this->drupalPostForm('admin/structure/block/manage/' . $block_id, $edit, t('Save block'));

View File

@ -30,13 +30,6 @@ class BlockFormTest extends UnitTestCase {
*/
protected $storage;
/**
* The event dispatcher service.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $dispatcher;
/**
* The language manager service.
*
@ -59,6 +52,13 @@ class BlockFormTest extends UnitTestCase {
*/
protected $entityManager;
/**
* The mocked context repository.
*
* @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $contextRepository;
/**
* {@inheritdoc}
*/
@ -67,7 +67,7 @@ class BlockFormTest extends UnitTestCase {
$this->conditionManager = $this->getMock('Drupal\Core\Executable\ExecutableManagerInterface');
$this->language = $this->getMock('Drupal\Core\Language\LanguageManagerInterface');
$this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
$this->contextRepository = $this->getMock('Drupal\Core\Plugin\Context\ContextRepositoryInterface');
$this->entityManager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface');
$this->storage = $this->getMock('Drupal\Core\Config\Entity\ConfigEntityStorageInterface');
@ -104,7 +104,7 @@ class BlockFormTest extends UnitTestCase {
->method('getQuery')
->will($this->returnValue($query));
$block_form_controller = new BlockForm($this->entityManager, $this->conditionManager, $this->dispatcher, $this->language, $this->themeHandler);
$block_form_controller = new BlockForm($this->entityManager, $this->conditionManager, $this->contextRepository, $this->language, $this->themeHandler);
// Ensure that the block with just one other instance gets the next available
// name suggestion.

View File

@ -60,7 +60,7 @@ class BlockRepositoryTest extends UnitTestCase {
]);
$theme_manager = $this->getMock('Drupal\Core\Theme\ThemeManagerInterface');
$theme_manager->expects($this->once())
$theme_manager->expects($this->atLeastOnce())
->method('getActiveTheme')
->will($this->returnValue($active_theme));
@ -85,9 +85,6 @@ class BlockRepositoryTest extends UnitTestCase {
$blocks = [];
foreach ($blocks_config as $block_id => $block_config) {
$block = $this->getMock('Drupal\block\BlockInterface');
$block->expects($this->once())
->method('setContexts')
->willReturnSelf();
$block->expects($this->once())
->method('access')
->will($this->returnValue($block_config[0]));
@ -102,7 +99,8 @@ class BlockRepositoryTest extends UnitTestCase {
->with(['theme' => $this->theme])
->willReturn($blocks);
$result = [];
foreach ($this->blockRepository->getVisibleBlocksPerRegion([]) as $region => $resulting_blocks) {
$cacheable_metadata = [];
foreach ($this->blockRepository->getVisibleBlocksPerRegion($cacheable_metadata) as $region => $resulting_blocks) {
$result[$region] = [];
foreach ($resulting_blocks as $plugin_id => $block) {
$result[$region][] = $plugin_id;
@ -147,9 +145,6 @@ class BlockRepositoryTest extends UnitTestCase {
*/
public function testGetVisibleBlocksPerRegionWithContext() {
$block = $this->getMock('Drupal\block\BlockInterface');
$block->expects($this->once())
->method('setContexts')
->willReturnSelf();
$block->expects($this->once())
->method('access')
->willReturn(AccessResult::allowed()->addCacheTags(['config:block.block.block_id']));
@ -158,14 +153,13 @@ class BlockRepositoryTest extends UnitTestCase {
->willReturn('top');
$blocks['block_id'] = $block;
$contexts = [];
$this->blockStorage->expects($this->once())
->method('loadByProperties')
->with(['theme' => $this->theme])
->willReturn($blocks);
$result = [];
$cacheable_metadata = [];
foreach ($this->blockRepository->getVisibleBlocksPerRegion($contexts, $cacheable_metadata) as $region => $resulting_blocks) {
foreach ($this->blockRepository->getVisibleBlocksPerRegion($cacheable_metadata) as $region => $resulting_blocks) {
$result[$region] = [];
foreach ($resulting_blocks as $plugin_id => $block) {
$result[$region][] = $plugin_id;

View File

@ -31,13 +31,6 @@ class BlockPageVariantTest extends UnitTestCase {
*/
protected $blockViewBuilder;
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $dispatcher;
/**
* The plugin context handler.
*
@ -71,12 +64,9 @@ class BlockPageVariantTest extends UnitTestCase {
$this->blockRepository = $this->getMock('Drupal\block\BlockRepositoryInterface');
$this->blockViewBuilder = $this->getMock('Drupal\Core\Entity\EntityViewBuilderInterface');
$this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
$this->dispatcher->expects($this->any())
->method('dispatch')
->willReturnArgument(1);
return $this->getMockBuilder('Drupal\block\Plugin\DisplayVariant\BlockPageVariant')
->setConstructorArgs(array($configuration, 'test', $definition, $this->blockRepository, $this->blockViewBuilder, $this->dispatcher, ['config:block_list']))
->setConstructorArgs(array($configuration, 'test', $definition, $this->blockRepository, $this->blockViewBuilder, ['config:block_list']))
->setMethods(array('getRegionNames'))
->getMock();
}
@ -217,23 +207,26 @@ class BlockPageVariantTest extends UnitTestCase {
$messages_block_plugin = $this->getMock('Drupal\Core\Block\MessagesBlockPluginInterface');
foreach ($blocks_config as $block_id => $block_config) {
$block = $this->getMock('Drupal\block\BlockInterface');
$block->expects($this->any())
->method('getContexts')
->willReturn([]);
$block->expects($this->atLeastOnce())
->method('getPlugin')
->willReturn($block_config[1] ? $main_content_block_plugin : ($block_config[2] ? $messages_block_plugin : $block_plugin));
$blocks[$block_config[0]][$block_id] = $block;
}
$this->blockViewBuilder->expects($this->exactly($visible_block_count))
->method('view')
->will($this->returnValue(array()));
$this->blockRepository->expects($this->once())
->method('getVisibleBlocksPerRegion')
->willReturnCallback(function ($contexts, &$cacheable_metadata) use ($blocks) {
->willReturnCallback(function (&$cacheable_metadata) use ($blocks) {
$cacheable_metadata['top'] = (new CacheableMetadata())->addCacheTags(['route']);
return $blocks;
});
$this->assertSame($expected_render_array, $display_variant->build());
$value = $display_variant->build();
$this->assertSame($expected_render_array, $value);
}
/**

View File

@ -345,7 +345,8 @@ function comment_entity_storage_load($entities, $entity_type) {
if (!\Drupal::entityManager()->getDefinition($entity_type)->isSubclassOf('Drupal\Core\Entity\FieldableEntityInterface')) {
return;
}
if (!\Drupal::service('comment.manager')->getFields($entity_type)) {
// @todo Investigate in https://www.drupal.org/node/2527866 why we need that.
if (!\Drupal::hasService('comment.manager') || !\Drupal::service('comment.manager')->getFields($entity_type)) {
// Do not query database when entity has no comment fields.
return;
}

View File

@ -24,3 +24,8 @@ services:
tags:
- { name: paramconverter }
lazy: true
language.current_language_context:
class: Drupal\language\ContextProvider\CurrentLanguageContext
arguments: ['@language_manager']
tags:
- { name: 'context_provider' }

View File

@ -2,22 +2,22 @@
/**
* @file
* Contains \Drupal\block\EventSubscriber\CurrentLanguageContext.
* Contains \Drupal\language\ContextProvider\CurrentLanguageContext.
*/
namespace Drupal\block\EventSubscriber;
namespace Drupal\language\ContextProvider;
use Drupal\block\Event\BlockContextEvent;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\ContextProviderInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Sets the current language as a context.
*/
class CurrentLanguageContext extends BlockContextSubscriberBase {
class CurrentLanguageContext implements ContextProviderInterface {
use StringTranslationTrait;
@ -41,10 +41,20 @@ class CurrentLanguageContext extends BlockContextSubscriberBase {
/**
* {@inheritdoc}
*/
public function onBlockActiveContext(BlockContextEvent $event) {
public function getRuntimeContexts(array $unqualified_context_ids) {
// Add a context for each language type.
$language_types = $this->languageManager->getLanguageTypes();
$info = $this->languageManager->getDefinedLanguageTypesInfo();
if ($unqualified_context_ids) {
foreach ($unqualified_context_ids as $unqualified_context_id) {
if (array_search($unqualified_context_id, $language_types) === FALSE) {
unset($language_types[$unqualified_context_id]);
}
}
}
$result = [];
foreach ($language_types as $type_key) {
if (isset($info[$type_key]['name'])) {
$context = new Context(new ContextDefinition('language', $info[$type_key]['name']));
@ -54,16 +64,18 @@ class CurrentLanguageContext extends BlockContextSubscriberBase {
$cacheability->setCacheContexts(['languages:' . $type_key]);
$context->addCacheableDependency($cacheability);
$event->setContext('language.' . $type_key, $context);
$result[$type_key] = $context;
}
}
return $result;
}
/**
* {@inheritdoc}
*/
public function onBlockAdministrativeContext(BlockContextEvent $event) {
$this->onBlockActiveContext($event);
public function getAvailableContexts() {
return $this->getRuntimeContexts([]);
}
}

View File

@ -45,3 +45,8 @@ services:
arguments: ['@current_user']
tags:
- { name: cache.context }
node.node_route_context:
class: Drupal\node\ContextProvider\NodeRouteContext
arguments: ['@current_route_match']
tags:
- { name: 'context_provider' }

View File

@ -2,22 +2,22 @@
/**
* @file
* Contains \Drupal\block\EventSubscriber\NodeRouteContext.
* Contains \Drupal\node\ContextProvider\NodeRouteContext.
*/
namespace Drupal\block\EventSubscriber;
namespace Drupal\node\ContextProvider;
use Drupal\block\Event\BlockContextEvent;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\ContextProviderInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\node\Entity\Node;
/**
* Sets the current node as a context on node routes.
*/
class NodeRouteContext extends BlockContextSubscriberBase {
class NodeRouteContext implements ContextProviderInterface {
/**
* The route match object.
@ -39,13 +39,13 @@ class NodeRouteContext extends BlockContextSubscriberBase {
/**
* {@inheritdoc}
*/
public function onBlockActiveContext(BlockContextEvent $event) {
public function getRuntimeContexts(array $unqualified_context_ids) {
$result = [];
$context = new Context(new ContextDefinition('entity:node', NULL, FALSE));
if (($route_object = $this->routeMatch->getRouteObject()) && ($route_contexts = $route_object->getOption('parameters')) && isset($route_contexts['node'])) {
if ($node = $this->routeMatch->getParameter('node')) {
$context->setContextValue($node);
}
$event->setContext('node.node', $context);
}
elseif ($this->routeMatch->getRouteName() == 'node.add') {
$node_type = $this->routeMatch->getParameter('node_type');
@ -54,15 +54,17 @@ class NodeRouteContext extends BlockContextSubscriberBase {
$cacheability = new CacheableMetadata();
$cacheability->setCacheContexts(['route']);
$context->addCacheableDependency($cacheability);
$event->setContext('node.node', $context);
$result['node'] = $context;
return $result;
}
/**
* {@inheritdoc}
*/
public function onBlockAdministrativeContext(BlockContextEvent $event) {
public function getAvailableContexts() {
$context = new Context(new ContextDefinition('entity:node'));
$event->setContext('node.node', $context);
return ['node' => $context];
}
}

View File

@ -2,23 +2,23 @@
/**
* @file
* Contains \Drupal\block\EventSubscriber\CurrentUserContext.
* Contains \Drupal\user\ContextProvider\CurrentUserContext.
*/
namespace Drupal\block\EventSubscriber;
namespace Drupal\user\ContextProvider;
use Drupal\block\Event\BlockContextEvent;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\ContextProviderInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Sets the current user as a context.
*/
class CurrentUserContext extends BlockContextSubscriberBase {
class CurrentUserContext implements ContextProviderInterface {
use StringTranslationTrait;
@ -52,7 +52,7 @@ class CurrentUserContext extends BlockContextSubscriberBase {
/**
* {@inheritdoc}
*/
public function onBlockActiveContext(BlockContextEvent $event) {
public function getRuntimeContexts(array $unqualified_context_ids) {
$current_user = $this->userStorage->load($this->account->id());
$context = new Context(new ContextDefinition('entity:user', $this->t('Current user')));
@ -60,14 +60,19 @@ class CurrentUserContext extends BlockContextSubscriberBase {
$cacheability = new CacheableMetadata();
$cacheability->setCacheContexts(['user']);
$context->addCacheableDependency($cacheability);
$event->setContext('user.current_user', $context);
$result = [
'current_user' => $context,
];
return $result;
}
/**
* {@inheritdoc}
*/
public function onBlockAdministrativeContext(BlockContextEvent $event) {
$this->onBlockActiveContext($event);
public function getAvailableContexts() {
return $this->getRuntimeContexts([]);
}
}

View File

@ -61,6 +61,11 @@ services:
user.permissions:
class: Drupal\user\PermissionHandler
arguments: ['@module_handler', '@string_translation', '@controller_resolver']
user.current_user_context:
class: Drupal\user\ContextProvider\CurrentUserContext
arguments: ['@current_user', '@entity.manager']
tags:
- { name: 'context_provider' }
parameters:
user.tempstore.expire: 604800

View File

@ -0,0 +1,152 @@
<?php
/**
* @file
* Contains \Drupal\Tests\Core\Plugin\Context\LazyContextRepositoryTest.
*/
namespace Drupal\Tests\Core\Plugin\Context;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\LazyContextRepository;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* @coversDefaultClass \Drupal\Core\Plugin\Context\LazyContextRepository
* @group context
*/
class LazyContextRepositoryTest extends UnitTestCase {
/**
* The container.
*
* @var \Symfony\Component\DependencyInjection\ContainerBuilder
*/
protected $container;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->container = new ContainerBuilder();
}
/**
* @covers ::getRuntimeContexts
*/
public function testGetRuntimeContextsSingle() {
$contexts = $this->setupContextAndProvider('test_provider', ['test_context']);
$lazy_context_repository = new LazyContextRepository($this->container, ['test_provider']);
$run_time_contexts = $lazy_context_repository->getRuntimeContexts(['@test_provider:test_context']);
$this->assertEquals(['@test_provider:test_context' => $contexts[0]], $run_time_contexts);
}
/**
* @covers ::getRuntimeContexts
*/
public function testGetRuntimeMultipleContextsPerService() {
$contexts = $this->setupContextAndProvider('test_provider', ['test_context0', 'test_context1']);
$lazy_context_repository = new LazyContextRepository($this->container, ['test_provider']);
$run_time_contexts = $lazy_context_repository->getRuntimeContexts(['@test_provider:test_context0', '@test_provider:test_context1']);
$this->assertEquals(['@test_provider:test_context0' => $contexts[0], '@test_provider:test_context1' => $contexts[1]], $run_time_contexts);
}
/**
* @covers ::getRuntimeContexts
*/
public function testGetRuntimeMultipleContextProviders() {
$contexts0 = $this->setupContextAndProvider('test_provider', ['test_context0', 'test_context1'], ['test_context0']);
$contexts1 = $this->setupContextAndProvider('test_provider2', ['test1_context0', 'test1_context1'], ['test1_context0']);
$lazy_context_repository = new LazyContextRepository($this->container, ['test_provider']);
$run_time_contexts = $lazy_context_repository->getRuntimeContexts(['@test_provider:test_context0', '@test_provider2:test1_context0']);
$this->assertEquals(['@test_provider:test_context0' => $contexts0[0], '@test_provider2:test1_context0' => $contexts1[1]], $run_time_contexts);
}
/**
* @covers ::getRuntimeContexts
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage You must provide the context IDs in the @{service_id}:{unqualified_context_id} format.
*/
public function testInvalidContextId() {
$lazy_context_repository = new LazyContextRepository($this->container, ['test_provider']);
$lazy_context_repository->getRuntimeContexts(['test_context', '@test_provider:test_context1']);
}
/**
* @covers ::getRuntimeContexts
*/
public function testGetRuntimeStaticCache() {
$context0 = new Context(new ContextDefinition('example'));
$context1 = new Context(new ContextDefinition('example'));
$context_provider = $this->prophesize('\Drupal\Core\Plugin\Context\ContextProviderInterface');
$context_provider->getRuntimeContexts(['test_context0', 'test_context1'])
->shouldBeCalledTimes(1)
->willReturn(['test_context0' => $context0, 'test_context1' => $context1]);
$context_provider = $context_provider->reveal();
$this->container->set('test_provider', $context_provider);
$lazy_context_repository = new LazyContextRepository($this->container, ['test_provider']);
$lazy_context_repository->getRuntimeContexts(['@test_provider:test_context0', '@test_provider:test_context1']);
$lazy_context_repository->getRuntimeContexts(['@test_provider:test_context0', '@test_provider:test_context1']);
}
/**
* @covers ::getAvailableContexts
*/
public function testGetAvailableContexts() {
$contexts0 = $this->setupContextAndProvider('test_provider0', ['test0_context0', 'test0_context1']);
$contexts1 = $this->setupContextAndProvider('test_provider1', ['test1_context0', 'test1_context1']);
$lazy_context_repository = new LazyContextRepository($this->container, ['test_provider0', 'test_provider1']);
$contexts = $lazy_context_repository->getAvailableContexts();
$this->assertEquals([
'@test_provider0:test0_context0' => $contexts0[0],
'@test_provider0:test0_context1' => $contexts0[1],
'@test_provider1:test1_context0' => $contexts1[0],
'@test_provider1:test1_context1' => $contexts1[1],
], $contexts);
}
/**
* Sets up contexts and context providers.
*
* @param string $service_id
* The service ID of the service provider.
* @param string[] $unqualified_context_ids
* An array of context slot names.
* @param string[] $expected_unqualified_context_ids
* The expected unqualified context IDs passed to getRuntimeContexts.
*
* @return array
* An array of set up contexts.
*/
protected function setupContextAndProvider($service_id, array $unqualified_context_ids, array $expected_unqualified_context_ids = []) {
$contexts = [];
for ($i = 0; $i < count($unqualified_context_ids); $i++) {
$contexts[] = new Context(new ContextDefinition('example'));
}
$expected_unqualified_context_ids = $expected_unqualified_context_ids ?: $unqualified_context_ids;
$context_provider = $this->prophesize('\Drupal\Core\Plugin\Context\ContextProviderInterface');
$context_provider->getRuntimeContexts($expected_unqualified_context_ids)
->willReturn(array_combine($unqualified_context_ids, $contexts));
$context_provider->getAvailableContexts()
->willReturn(array_combine($unqualified_context_ids, $contexts));
$context_provider = $context_provider->reveal();
$this->container->set($service_id, $context_provider);
return $contexts;
}
}