Commit 794be163 authored by alexpott's avatar alexpott

Issue #2354889 by larowlan, dawehner, lauriii, Berdir, catch, martin107,...

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
parent 46e6f723
......@@ -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,
......
......@@ -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']
......
......@@ -10,6 +10,7 @@
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 @@ public function register(ContainerBuilder $container) {
// 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());
......
<?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);
}
}
<?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();
}
<?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();
}
<?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;
}
}
......@@ -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']
......@@ -18,6 +18,7 @@
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 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
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 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
* 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 @@ protected function checkAccess(EntityInterface $entity, $operation, $langcode, A
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) {
......
......@@ -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\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 @@ public static function create(ContainerInterface $container) {
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 @@ public function form(array $form, FormStateInterface $form_state) {
// 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);
......
......@@ -95,24 +95,6 @@ public function getVisibilityCondition($instance_id);
*/
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).
*
......
......@@ -50,7 +50,7 @@ public function __construct(EntityManagerInterface $entity_manager, ThemeManager
/**
* {@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 @@ public function getVisibleBlocksPerRegion(array $contexts, array &$cacheable_met
$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])) {
......
......@@ -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 = []);
}
......@@ -250,21 +250,6 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) {
}
}
/**
* {@inheritdoc}
*/
public function setContexts(array $contexts) {
$this->contexts = $contexts;
return $this;
}
/**
* {@inheritdoc}
*/
public function getContexts() {
return $this->contexts;
}
/**
* {@inheritdoc}
*/
......
<?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;
}
}
<?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';
}
<?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
</