Proposed commit message:

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

Problem/Motivation

During the usage of the webprofiler on a stock Drupal (no content needed etc., just standard install) I realized that the following events needed not 0 time as I would have expected it: 'block.condition_context'

What is this event doing. There are by default three subscribers for determining the context of a block:

  • NodeRouteContext
  • CurrentUserContext
  • CurrentLanguageContext

None of them actually differ from block to block, they are all "global" contexts.

Also even if no blocks are configured to use them, they all run on every HTML page request. In core the most expensive (particularly on anonymous page requests) is #2479529: \Drupal\user\ContextProvider\CurrentUserContext::onBlockActiveContext() loads the anonymous user, but contrib is expected to add more contexts.

Proposed resolution

  • Have the block visibility configuration store a reference to the service which provided the context.
  • When checking block visibility, only initialize the contexts required by blocks placed in the active theme
  • Maintain the optimization where we only initialize global block contexts once per request (previously they were running for every block)
  • Make this logic and pattern re-usable for contrib

Remaining tasks

Reviews

User interface changes

None

API changes

BlockEvent and BlockEvents are removed
BlockSubscriberBase is removed
Context providing services must be tagged as context_provider and implement Drupal\Core\Plugin\Context\ContextProviderInterface
BlockRepositoryInterface changes thus:

-  public function getVisibleBlocksPerRegion(array $contexts, array &$cacheable_metadata = []);
+  public function getVisibleBlocksPerRegion(array &$cacheable_metadata = []);

Data model changes

Visibility conditions now store a reference to the service ID as well as the context slot name in their mapping.

Before

visibility:
  node_type:
    id: node_type
    bundles:
      article: article
    negate: false
    context_mapping:
      node: node.node

After

visibility:
  node_type:
    id: node_type
    bundles:
      article: article
    negate: false
    context_mapping:
      node: @node.node_route_context:node

Profiling (from #209

### FINAL REPORT

=== 8.0.x..8.0.x compared (559b0b0e56140..559b0c03d41d6):

ct  : 27,504|27,504|0|0.0%
wt  : 64,377|64,306|-71|-0.1%
mu  : 14,889,272|14,889,272|0|0.0%
pmu : 14,956,152|14,956,152|0|0.0%

=== 8.0.x..2354889 compared (559b0b0e56140..559b0c10cc4dc):

ct  : 27,504|26,892|-612|-2.2%
wt  : 64,377|61,858|-2,519|-3.9%
mu  : 14,889,272|14,157,288|-731,984|-4.9%
pmu : 14,956,152|14,218,696|-737,456|-4.9%

---
ct = function calls, wt = wall time, cpu = cpu time used, mu = memory usage, pmu = peak memory usage

### XHPROF-LIB REPORT

+-------------------+------------+------------+------------+------------+------------+
| namespace         |        min |        max |       mean |     median |       95th |
+-------------------+------------+------------+------------+------------+------------+
| Calls             |            |            |            |            |            |
|                   |            |            |            |            |            |
| 2354889           |     26,892 |     28,579 |     26,909 |     26,892 |     26,892 |
| 8_0_x             |     27,504 |     29,334 |     27,522 |     27,504 |     27,504 |
|                   |            |            |            |            |            |
| Wall time         |            |            |            |            |            |
|                   |            |            |            |            |            |
| 2354889           |     61,858 |     76,623 |     64,582 |     64,135 |     68,569 |
| 8_0_x             |     64,306 |     93,565 |     68,014 |     67,082 |     74,414 |
|                   |            |            |            |            |            |
| Memory usage      |            |            |            |            |            |
|                   |            |            |            |            |            |
| 2354889           | 14,157,120 | 14,557,944 | 14,189,594 | 14,157,288 | 14,509,872 |
| 8_0_x             | 14,889,232 | 15,299,768 | 14,932,275 | 14,889,272 | 15,242,024 |
|                   |            |            |            |            |            |
| Peak memory usage |            |            |            |            |            |
|                   |            |            |            |            |            |
| 2354889           | 14,218,696 | 14,685,672 | 14,251,873 | 14,218,696 | 14,570,544 |
| 8_0_x             | 14,956,112 | 15,429,384 | 14,999,913 | 14,956,152 | 15,307,784 |
|                   |            |            |            |            |            |
+-------------------+------------+------------+------------+------------+------------+
CommentFileSizeAuthor
#242 2354889-242.patch60.33 KBdawehner
#242 interdiff.txt4.81 KBdawehner
#239 interdiff.txt9.89 KBdawehner
#239 2354889-239.patch60.86 KBdawehner
#228 2354889-228-interdiff.txt10.9 KBBerdir
#228 2354889-228.patch58.96 KBBerdir
#224 2354889-224-interdiff.txt23.69 KBBerdir
#224 2354889-224.patch60.04 KBBerdir
#222 2512866-111-interdiff.txt15.11 KBBerdir
#222 2512866-111.patch50.49 KBBerdir
#218 interdiff.txt3.06 KBdawehner
#218 2354889-218.patch59.76 KBdawehner
#214 block-context-2354889-2.214.patch60.04 KBlarowlan
#214 interdiff.txt594 byteslarowlan
#210 block-context-2354889-2.210.patch60.04 KBlarowlan
#210 interdiff.txt10.71 KBlarowlan
#208 block-context-2354889-2.208.patch60.18 KBlarowlan
#208 interdiff.txt28.27 KBlarowlan
#196 context-manager-simplify-2354889-interdiff.txt12.21 KBBerdir
#195 interdiff.txt10.83 KBdawehner
#195 2354889-195.patch59.02 KBdawehner
#194 interdiff.txt8.62 KBdawehner
#194 2354889-191.patch58.94 KBdawehner
#188 interdiff.txt6.25 KBdawehner
#188 2354889-187.patch57.75 KBdawehner
#186 interdiff.txt15.17 KBdawehner
#186 2354889-184.patch56.26 KBdawehner
#178 block-context-2354889-2.178.patch53.18 KBlarowlan
#178 interdiff.txt1.48 KBlarowlan
#176 block-context-2354889-2.176.patch53.28 KBlarowlan
#176 interdiff.txt1.82 KBlarowlan
#173 block-context-2354889-2.173.patch53.27 KBlarowlan
#173 interdiff.txt17 KBlarowlan
#170 block-context-2354889-2.168.patch53.8 KBlarowlan
#170 interdiff.txt750 byteslarowlan
#167 block-context-2354889-2.167.patch53.81 KBlarowlan
#166 block-context-2354889-2.166.patch261.17 KBlarowlan
#166 interdiff.txt9.81 KBlarowlan
#163 interdiff.txt24.85 KBdawehner
#163 2354889-160.patch55.65 KBdawehner
#158 interdiff.txt910 bytesdawehner
#158 2354889-158.patch54.21 KBdawehner
#154 interdiff.txt3.91 KBlauriii
#154 make_block_context-2354889-153.patch60.5 KBlauriii
#151 interdiff.txt8.75 KBlauriii
#151 make_block_context-2354889-151.patch58.47 KBlauriii
#147 block-context-2354889-2.147.patch53.98 KBlarowlan
#147 interdiff.txt3.9 KBlarowlan
#145 block-context-2354889-2.145.patch54.01 KBlarowlan
#145 interdiff.txt37.41 KBlarowlan
#140 interdiff.txt7.86 KBpfrenssen
#140 2354889-140.patch57.73 KBpfrenssen
#122 block-context-2354889-2.122.patch51.29 KBlarowlan
#122 interdiff.txt5.58 KBlarowlan
#117 block-context-2354889-2.117.patch51.28 KBlarowlan
#117 interdiff.txt694 byteslarowlan
#111 block-context-2354889-2.111.patch51.28 KBlarowlan
#111 interdiff.txt3.25 KBlarowlan
#108 block-context-2354889-2.108.patch51.18 KBlarowlan
#108 interdiff.txt41.49 KBlarowlan
#104 interdiff.txt7.9 KBlauriii
#104 make_block_context-2354889-104.patch31.34 KBlauriii
#102 make_block_context-2354889-102.patch32.1 KBlauriii
#94 block-context-2354889.94.patch32.73 KBlarowlan
#94 interdiff.txt3.33 KBlarowlan
#92 block-context-2354889.92.patch30.58 KBlarowlan
#92 interdiff.txt591 byteslarowlan
#90 block-context-2354889.90.patch30.2 KBlarowlan
#90 interdiff.txt16.18 KBlarowlan
#88 block-context-2354889.87.patch24.95 KBlarowlan
#78 block-context-2354889.78.patch25.43 KBlarowlan
#78 interdiff.txt7.15 KBlarowlan
#77 block-context-2354889.75.patch25.38 KBlarowlan
#77 interdiff.txt741 byteslarowlan
#74 block-context-2354889.74.patch25.38 KBlarowlan
#74 interdiff.txt3.51 KBlarowlan
#72 block-context-2354889.71.patch26.91 KBlarowlan
#72 interdiff.txt1.87 KBlarowlan
#65 2354889-65.patch10.95 KBEclipseGc
#63 block-context-2354889.63.patch26.18 KBlarowlan
#63 interdiff.txt1.24 KBlarowlan
#60 block-context-2354889.60.patch24.94 KBlarowlan
#60 interdiff.txt6.7 KBlarowlan
#52 block-context-2354889.53.patch19.19 KBlarowlan
#52 interdiff.txt2.78 KBlarowlan
#48 interdiff-44-48.txt751 bytesmartin107
#48 block-context-2354889.48.do-not-test.patch.patch19.53 KBmartin107
#44 block-context-2354889.44.do-not-test.patch.patch19.11 KBlarowlan
#41 block-context-2354889.41.do-not-test.patch8.41 KBlarowlan
#41 interdiff.txt5.44 KBlarowlan
#34 Screenshot 2015-06-14 09.02.15.png43.26 KBlarowlan
#27 block-context-2354889.27.patch4.37 KBlarowlan
#12 2354889.patch4.36 KBcatch
#12 Screen Shot 2015-04-28 at 6.24.14 PM.png98.95 KBcatch
#12 Screen Shot 2015-04-28 at 6.22.52 PM.png100.52 KBcatch
#1 2354889-1.patch5.07 KBdawehner
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

dawehner’s picture

Status: Active » Needs review
FileSize
5.07 KB

No content at all

Using the !wonderful! xhprof-kit (hacked it to get it working with uprofiler) I got to the following result using no content (yes I know this is kind of cheating as the view/render is less important with that,
but let's assume we would have a cache HIT on the main content ...

/v/w/d8 (simplify-context) $ ~/bin/xhprof-kit/benchmark-branches.sh 5439a4a0dbd50 8.0.x simplify-context
=== 8.0.x..simplify-context compared (5439a4a0dbd50..5439a5d0edc8d):

ct  : 63,800|62,336|-1,464|-2.3%
wt  : 166,872|164,729|-2,143|-1.3%


---
ct = function calls, wt = wall time, cpu = cpu time used, mu = memory usage, pmu = peak memory usage

Maybe this is indeed worth to have a look at. In general I am not sure whether the approach is the actual proper one. The context system should be aware of global contexts and just inject them as fast as possible.
Otherwise we could also have a base context class for global ones which allows you to get rid of the requirement to do the one-time loading for your own.

50 frontpage articles

/v/w/d8 (8.0.x) $ ~/bin/xhprof-kit/benchmark-branches.sh 5439a82401e7c 8.0.x simplify-context
=== 8.0.x..simplify-context compared (5439a82401e7c..5439a84f3b39a):

ct  : 95,093|93,629|-1,464|-1.5%
wt  : 251,883|244,833|-7,050|-2.8%


---
dawehner’s picture

Issue summary: View changes
dawehner’s picture

Issue summary: View changes
Fabianx’s picture

I must say I am at a loss how event subscribers work, how this keeps state and how this->addContext() differs from the other internal class state.

The benchmark result looks great though.

dawehner’s picture

@eclipseGC asked me to move things into the base class but given that you have to know the name of the context I decided to not do that. We can do that always later but should move a bit more forward and not discuss things for the sake of it forever.

EclipseGc’s picture

Status: Needs review » Postponed

So, as part of #2339151: Conditions / context system does not allow for multiple configurable contexts, eg. language types I've had to touch this topic. That issue is moving context gathering to FullPageVariant and it happens once and is injected in the block plugins & entities for blocks/conditions to leverage as necessary. How about we postpone this issue on that one and if that lands we can close this, and if, for whatever reason I can't get that issue in, we can revisit this one.

Eclipse

Berdir’s picture

I noticed similar issues in page_manager.

dawehner’s picture

Status: Postponed » Fixed

The other issue fixed that problem.

Wim Leers’s picture

Indeed! :)

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

catch’s picture

Status: Closed (fixed) » Active

Re-opening - I see this taking 7-10ms on the front page, minimal profile.

- we get the global contexts even if no visibility is configured - this takes 4ms in TypedData, also #2479529: \Drupal\user\ContextProvider\CurrentUserContext::onBlockActiveContext() loads the anonymous user and then other bits here and there.

catch’s picture

Issue summary: View changes
Status: Active » Needs review
FileSize
100.52 KB
98.95 KB
4.36 KB

Here's a very rough patch, works enough for profiling, interesting to see how much it explodes.

1. Don't get the contexts up front.

2. Do get the contexts when they're actually needed.

#2 should not work like this - we really want to get the contexts needed by the visiblity plugins, not all of them at once, and not use a nasty hack like the one in here.

dawehner’s picture

That is certainly some progress but I guess we should really apply the proper strategy:

  • Figure out which contexts each block need (maybe do that in beforehand)
  • Just fetch the contexts which are actually needed

Given that those contexts also matter for rules, I guess changing the API and moving away from events would have an impact, but especially
if its a positive one performance wise we should consider doing it.

Status: Needs review » Needs work

The last submitted patch, 12: 2354889.patch, failed testing.

catch’s picture

Priority: Normal » Major

EclipseGc asked in irc if I could profile a node page. The node context takes about 0.8ms on a node page so not much different.

I think this comes down to:

~4ms for TypedData stuff.

~1ms for each context

Works out as ~8ms with minimal profile.

Bumping to major, but this is borderline critical if it requires an API change per #1744302: [meta] Resolve known performance regressions in Drupal 8. Even with SmartCache we'd still need to run this code on every cache miss below SmartCache.

andypost’s picture

So maybe better to keep global contexts cached somewhere to not dispatch event on each region?

  1. +++ b/core/modules/block/src/BlockAccessControlHandler.php
    @@ -44,6 +46,11 @@ class BlockAccessControlHandler extends EntityAccessControlHandler implements En
    +   * The global block contexts.
    ...
    +  protected $contexts;
    
    @@ -85,18 +92,24 @@ protected function checkAccess(EntityInterface $entity, $operation, $langcode, A
    +          if ($condition instanceof ContextAwarePluginInterface) {
    +            try {
    +              $contexts = $entity->getContexts();
    +              $this->contextHandler->applyContextMapping($condition, $contexts);
    

    If condition knows about needed contexts so it could be $entity->getNeededContexts($condition) somehow

  2. +++ b/core/modules/block/src/BlockAccessControlHandler.php
    @@ -115,4 +128,14 @@ protected function checkAccess(EntityInterface $entity, $operation, $langcode, A
    +    if (!isset($this->contexts)) {
    +      $this->contexts = \Drupal::service('event_dispatcher')->dispatch(BlockEvents::ACTIVE_CONTEXT, new BlockContextEvent())->getContexts();
    

    probably that disatcher needs injection

Wim Leers’s picture

Quoting IRC:

WimLeers: EclipseGc: well, blocks could do something similar to what Views does. Views calculates the cacheability of each display at save time, so that at run time, it only has to load it from config, which it loads anyway. Similarly, blocks could analyze which contexts it needs, indicate that in Block config entity at save time, and then you have that information, very cheaply.

See #2318377: Determine whether a view is cacheable and its required contexts, store this i/t config entity for the history around that, and hopefully for inspiration.

EclipseGc’s picture

I'm curious how cheap that would be. With #2375695: Condition plugins should provide cache contexts AND cacheability metadata needs to be exposed it would be very feasible to calculate the cacheability of a block on save, however that data is still serialized into the config table, and requires entity loading every block in the system to first determine which blocks are used for this theme and then extract the cacheability data. Unless we want to introduce new tables to query that specific information, which would make Block a very different sort of config entity from the rest of core. That's fine, I'm just pointing it out.

Most of the damage here seems to be initializing typed data... no?

Eclipse

catch’s picture

and requires entity loading every block in the system to first determine which blocks are used for this theme and then extract the cacheability data.

We already do that every request (give or take a config entity query to get blocks for the theme) - see #2479459: BlockRepository::getVisibleBlocksPerRegion() does an uncached entity query on every request which tries to wrap the block querying and loading in a cache. So having the required contexts stored in the block entities would add very little if anything given we already do this.

Wim Leers’s picture

and requires entity loading every block in the system to first determine which blocks are used for this theme and then extract the cacheability data.

We already do that every request

Exactly!

See:

  public function getVisibleBlocksPerRegion(array $contexts) {
    // Build an array of the region names in the right order.
    $empty = array_fill_keys(array_keys($this->getRegionNames()), array());

    $full = array();
    foreach ($this->blockStorage->loadByProperties(array('theme' => $this->getTheme())) as $block_id => $block) {
      /** @var \Drupal\block\BlockInterface $block */
      // Set the contexts on the block before checking access.
      if ($block->setContexts($contexts)->access('view')) {
        $full[$block->getRegion()][$block_id] = $block;
      }
    }

So we receive all the contexts (that were so expensive to retrieve), to then NOT use them at all during the loading of block config entities. It's only AFTER we have loaded all block config entities that we start using contexts.

That's why I like that approach: small set of changes, yet still big impact.

EclipseGc’s picture

My point is that that would all happen before we ever do any of getVisibleBlocksPerRegion() because that would be required to know which contexts will actually be used. That's fine I suppose, but that's the layer at which it should happen.

Eclipse

Fabianx’s picture

Yes I agree.

If we think of this as a 'page' object (which we simulate) we have:

- Block IDs
- Contexts those blocks use
- Conditions those blocks use

If I was to design that I would use a very simple object to store that data per theme, e.g.:

block_ids: [ 'header' => ['block_1', 'block_2'] ]
contexts: [ 'node' ]
conditions: ['node_type']

However why not simulate that by caching that information in this exact fashion after building it once? Might take a little moment longer (but could be deferred to build after saving as write-through cache) but then we have all information available to efficiently do the same:

- Get contexts => we already know which contexts those blocks expect, so no need to load more of them.
- Load blocks by region (we already know that information, no need to query)
- Load all condition plugins that are used at once (saves some time, too - possibly less discovery)

- Visibility checks
- Then one block_load_multiple
- Then building the individual regions

That is similar to what catch tries to do in that other issue (but only for the blocks), but caching in this fashion seems the most clean to me.

Am I missing something?

dawehner’s picture

I agree that introducing some form of abstraction for an internal optimization is kinda odd, especially given that the way how panels will solve that problem is probably 100% different,
so that information could not be shared.

Fabianx’s picture

Title: Make block context faster » Make block context faster by removing onBlock event and replace it with loading from a BlockContextManager

Per #2375695-83: Condition plugins should provide cache contexts AND cacheability metadata needs to be exposed this is really important to do. An architecture that always loads all condition and other plugins via an event does per design not scale.

To quote myself:

What if I have 100s of contexts on a site? They will all run automatically and load everything that might apply?

I think introducing onBlock event was an architectural mistake (and hence the criticality as IMHO the only sane way is to remove the onBlock event again).

There is a language for contexts, so we know exactly what each block has for context, so a block should ask a ContextManager service for the contexts it needs (lazy loaded for all blocks together for speed reasons).

Just because SCOTCH did not go in, does not mean we can take short cuts, we have that debt and we need to solve it properly.

And a service and using "don't call us, we'll call you" approach seems to be the sanest.

Removing onBlock however is something we cannot do after 8.0.0 ...

catch’s picture

Priority: Major » Critical

Yes I have been thinking about making this critical for a while, but held off because the saving with standard profile is not quite 10ms (although it is not shabby, just #2479529: \Drupal\user\ContextProvider\CurrentUserContext::onBlockActiveContext() loads the anonymous user would be an improvement of over 3ms.

However the fact that we'll get more block contexts added by contrib, and they'll also always run on every request, does make this a 'performance critical' - see note about contrib modules here:https://www.drupal.org/core/issue-priority

Over ~10ms savings with warm caches and would have to be deferred to 9.x
[snip]
Gets measurably worse with lots of contrib modules or large data sets (e.g. non-indexed queries) and would have to be deferred to a minor version

Also as a context author, I'd never expect my context to be called unless actually in use, so just like the user context, it doesn't look like it should be in the critical path, when currently it is.

catch’s picture

larowlan’s picture

Status: Needs work » Needs review
FileSize
4.37 KB

re-roll

catch’s picture

Issue summary: View changes

Status: Needs review » Needs work

The last submitted patch, 27: block-context-2354889.27.patch, failed testing.

The last submitted patch, 27: block-context-2354889.27.patch, failed testing.

larowlan’s picture

Issue summary: View changes

Updating issue summary to include notes from the weekly criticals meeting.

Wim Leers’s picture

+1 to #24, #25 and #31.

larowlan’s picture

Assigned: Unassigned » larowlan

Taking a look

larowlan’s picture

Assigned: larowlan » Unassigned
Issue summary: View changes
FileSize
43.26 KB

Looks like these are already being saved in the config - note the context mapping:

uuid: 6d9d4efc-1bed-4e59-a55a-9087fd612e72
langcode: en
status: true
dependencies:
  module:
    - search
    - user
  theme:
    - bartik
id: bartik_search
theme: bartik
region: sidebar_first
weight: -1
provider: null
plugin: search_form_block
settings:
  id: search_form_block
  label: Search
  provider: search
  label_display: visible
  cache:
    max_age: -1
visibility:
  user_role:
    id: user_role
    roles:
      administrator: administrator
    negate: false
    context_mapping:
      user: user.current_user

The on block event also fires in an administrative context - see for example CurrentUserContext::onBlockAdministrativeContext

And then ConditionPluginBase::buildConfigurationForm() also calls addContextAssignmentElement which adds this element

if (count($options) > 1) {
        $assignments = $plugin->getContextMapping();
        $element[$context_slot] = [
          '#title' => $this->t('Select a @context value:', ['@context' => $context_slot]),
          '#type' => 'select',
          '#options' => $options,
          '#required' => $definition->isRequired(),
          '#default_value' => !empty($assignments[$context_slot]) ? $assignments[$context_slot] : '',
        ];
      }

So it looks like this is already being saved and we already show the form element - we just need to utilize it?

To test this, I added a 'user from route' context and here's the screenshot of it in action.

Thoughts?

dawehner’s picture

Great research @larowlan, its great that we have that part already!

To be clear, these contexts are added via subclasses of \Drupal\block\EventSubscriber\BlockContextSubscriberBase,
see

    $context = new Context(new ContextDefinition('entity:user', $this->t('Current user')));
    $context->setContextValue($current_user);
    $event->setContext('user.current_user', $context);

so much like for cache contexts we need a mapping between these machine names and their corresponding context provider, so we can ask them directly.

Current flow

What are we doing at the moment:

  • When a block is rendered we throw the event \Drupal\block\Event\BlockEvents::ACTIVE_CONTEXT
  • In the block form we throw the event \Drupal\block\Event\BlockEvents::ADMINISTRATIVE_CONTEXT which then allows you to collect the contexts which are available

Suggested flow

(Note this tries to be as minimal as possible)

Given how similar contexts here are to cache contexts, we should try to build a similar model. Maybe one day we could even partially unify them, but I guess this is tricky,
given its typed data requirement.

  • Convert the "context providers" (currently the subclasses of \Drupal\block\EventSubscriber\BlockContextSubscriberBase to a tagged service, similar to cache contexts,
  • Store a mapping between the providing service and the cache context it provides. This just needs to happen once. (Stored in BlockContextManager)
  • On admin time call out to all of them, like we currently now and provide a user interface.
  • We could also store the used cache contexts
  • Use the mapping provided above to just call out to the block contexts on runtime we need
  • For #2375695: Condition plugins should provide cache contexts AND cacheability metadata needs to be exposed we could then reuse the information easily
larowlan’s picture

Convert the "context providers" (currently the subclasses of \Drupal\block\EventSubscriber\BlockContextSubscriberBase to a tagged service, similar to cache contexts,

Yeah they're already service (event subscribers) but the service ID doesn't match the context mapping, so we'd need to align those first.

Will start there

Berdir’s picture

Keep in mind that a single event subscriber can add expose multiple (dynamic) contexts and we must support that. So we can't just 1:1 map the service names and the context names.

For example, page manager has a subscriber that exposes route parameters as context and there's a patch to expose configurable contexts.. with any possible name.

We can't statically map that.

dawehner’s picture

Keep in mind that a single event subscriber can add expose multiple (dynamic) contexts and we must support that. So we can't just 1:1 map the service names and the context names.

Is there no way to at least know the list of possible once in advance? It sounds architecturally wrong to require to execute all of them all the time, even you just want one.

We can't statically map that.

Well route parameters are context are still one context, if you think about how cache contexts work, it could be statically mappable, in the sense that there would be an additional resolve step in there.

larowlan’s picture

Any reason why we can't store the service ID as well as part of the context mapping?

visibility:
  user_role:
    id: user_role
    roles:
      administrator: administrator
    negate: false
    context_mapping:
-      user: user.current_user
+     user: user.current_user@block.current_user_context

That way we continue to execute all events in the admin context, but ensure the config contains the service IDs we need for non-admin runtime.

larowlan’s picture

Issue tags: +D8 upgrade path

Possibly upgrade path blocker for config changes

larowlan’s picture

Status: Needs work » Needs review
FileSize
5.44 KB
8.41 KB

All I have time for today, WIP

EclipseGc’s picture

@larowlan,

Your research into block context & form elements for mapping might benefit from checking out #2377757: Expose Block Context mapping in the UI. This is the issue where I've been working to get configurable block contexts in core exposed the same way they are for conditions (which is why it was built generically and re-useably.

@dawhener

Berdir is correct, and no the route context providing capabilities are not limited 1 to 1. A single route could define multiple contexts across the url parameters and (I think) even in the route defaults if properly approached. (Not horribly relevant just saying I think it's technically possible). The problem here isn't that it's impossible to get a list of available contexts, it's that core makes this incredibly difficult by not actually having any notion of block placement on a per page basis. Core just configures blocks for all pages simultaneously and that just simply insane. The caching layer we've built expects to functionally calculate all of this information, but the second we allow stuff from the url (which is sort of a sane expectation) you end up needing to calculate all of that across all pages because Drupal has no notion of page management.

In a true page_manager type scenario, this would be much easier cause we'd just ask the page object what contexts are relevant for this page variant. Even calculating all variants at once would be pretty easy in 99% of the use cases and some sane expectation can be force down on that other 1%.

Eclipse

larowlan’s picture

Assigned: Unassigned » larowlan

kicking some more

larowlan’s picture

Assigned: larowlan » Unassigned
FileSize
19.11 KB

More WIP, out of time - implements most of 35 but haven't looked at tests manual or otherwise

larowlan’s picture

Issue summary: View changes

Adding API changes

Berdir’s picture

As an alternative idea that would possibly have a smaller API change, what if we'd just pass the list of contexts IDs that we need to the services that provide them? It would mean they are responsible for checking that and only add them if necessary but it would probably help quite a bit already, assuming that calling those services isn't too expensive but creating the contexts is.

Might be worth looking into if we run into troubles with this approach?

Status: Needs review » Needs work

The last submitted patch, 44: block-context-2354889.44.do-not-test.patch.patch, failed testing.

martin107’s picture

Status: Needs work » Needs review
FileSize
19.53 KB
751 bytes

This minor nudge, makes BlockHtmlTest pass.

Status: Needs review » Needs work

The last submitted patch, 48: block-context-2354889.48.do-not-test.patch.patch, failed testing.

tim.plunkett’s picture

+++ b/core/modules/block/src/BlockAccessControlHandler.php
@@ -85,18 +92,24 @@ protected function checkAccess(EntityInterface $entity, $operation, $langcode, A
+        $contexts = $entity->getContexts();
+        if (!isset($contexts)) {
+          $contexts = $this->getContexts();
+        }
...
+              $contexts = $entity->getContexts();

That looks broken

dsnopek’s picture

Issue tags: +D8panels
larowlan’s picture

Status: Needs work » Needs review
FileSize
2.78 KB
19.19 KB

This should be ready for tests, page renders ok, only needed contexts are loaded.

Berdir’s picture

Crossposting my comment in #2375695-86: Condition plugins should provide cache contexts AND cacheability metadata needs to be exposed.

Wondering if someone here, especially @larowlan, has any ideas/suggestions on the other issue and if this could simplify the other one. The primary conflicting point over there seems to be that the services need to return context objects without a value even if there is no context, so that we can transport the cacheability metadata. *Maybe* this could support that in a more direct way, e.g. by exposing the cacheability metadata directly with a separate method or so. But we could have multiple contexts with different metadata and we still need a way to transport it...

jibran’s picture

+++ b/core/modules/block/src/BlockAccessControlHandler.php
@@ -85,18 +92,20 @@ protected function checkAccess(EntityInterface $entity, $operation, $langcode, A
+      if ($entity->getVisibilityConditions()) {
+        foreach ($entity->getVisibilityConditions() as $condition_id => $condition) {

Why not make

 if ($visibility_conditions = $entity->getVisibilityConditions()) {
foreach($visibility_conditions as ...)?
dawehner’s picture

*Maybe* this could support that in a more direct way, e.g. by exposing the cacheability metadata directly with a separate method or so. But we could have multiple contexts with different metadata and we still need a way to transport it...

I think that would be not a bad idea, especially because this could make that information 100% cacheable as this kind of special interface would ensure that things aren't dynamic on runtime ...

larowlan’s picture

@Berdir will try and have a look over there, but my bandwidth is uber-low until at least the 24th so it might take a while

Status: Needs review » Needs work

The last submitted patch, 52: block-context-2354889.53.patch, failed testing.

Fabianx’s picture

Overall: Fantastic work!

Some comments:

  1. +++ b/core/modules/block/block.services.yml
    @@ -12,16 +12,29 @@ services:
    +    class: Drupal\block\BlockContextManager
    +    tags:
    +      - { name: service_collector, tag: block_context_provider, call: addContextProvider }
    

    I think it would be better to do it like cache_context_manager, to only collect service tag IDs and make that service container aware.

    Injecting all possible block_context_provider's feels wrong ...

  2. +++ b/core/modules/block/src/BlockAccessControlHandler.php
    @@ -85,18 +92,20 @@ protected function checkAccess(EntityInterface $entity, $operation, $langcode, A
    +      if ($entity->getVisibilityConditions()) {
    +        foreach ($entity->getVisibilityConditions() as $condition_id => $condition) {
    

    I agree with Jibran, that we should store the result.

  3. +++ b/core/modules/block/src/BlockContextManager.php
    @@ -0,0 +1,40 @@
    +  public function getContextProvider($id) {
    +    if (isset($this->contextProviders[$id])) {
    +      return $this->contextProviders[$id];
    +    }
    +    throw new \InvalidArgumentException(sprintf('You tried to request a non-existent context-provider (%s)', $id));
    +  }
    

    I think it would be better to have a more specific interface that runs the onBlockActiveContext() event, so that this information is hidden from users of this service.

    Unless there is a need to get a specific context provider, but if you go to that length, why not get it from the container itself?

  4. +++ b/core/modules/block/src/EventSubscriber/CurrentLanguageContext.php
    @@ -48,7 +48,7 @@ public function onBlockActiveContext(BlockContextEvent $event) {
    -        $event->setContext('language.' . $type_key, $context);
    +        $event->setContext('language.' . $type_key . '@block.current_language_context', $context);
    
    +++ b/core/modules/block/src/EventSubscriber/CurrentUserContext.php
    @@ -56,7 +56,7 @@ public function onBlockActiveContext(BlockContextEvent $event) {
    -    $event->setContext('user.current_user', $context);
    +    $event->setContext('user.current_user@block.current_user_context', $context);
    

    Should this not try to use the ID the service currently has?

    Or if we want to hardcode it, we should also add it to the docs of the class.

    e.g. currently $this->_serviceId.

    Not sure we have a method for that ...

  5. +++ b/core/modules/block/src/EventSubscriber/UserRouteContext.php
    @@ -0,0 +1,61 @@
    +      $event->setContext('user.user', $context);
    ...
    +    $event->setContext('user.user', $context);
    

    Is this not missing the new '@' notation?

  6. +++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
    @@ -128,9 +135,24 @@ public function build() {
    +    $block_event = new BlockContextEvent();
    +    foreach ($active_blocks as $block_id => $block) {
    +      /* @var \Drupal\block\BlockInterface $block */
    +      foreach ($block->getVisibilityConditions() as $condition) {
    +        if ($condition instanceof ContextAwarePluginInterface) {
    +          /* @var \Drupal\Core\Plugin\ContextAwarePluginInterface $condition */
    +          $mapping = $condition->getContextMapping();
    +          foreach ($mapping as $context_slot => $context_provider_id) {
    +            list(, $context_service_id) = explode('@', $context_provider_id);
    +            $this->contextManager->getContextProvider($context_service_id)->onBlockActiveContext($block_event);
    +          }
    +        }
    

    Imho this is way too much information, especially with using the BlockContextEvent().

    I would imagine the API to be like:

    // Collect all conditions

    $context_plugins = [];
    if ($condition instanceof ContextAwarePluginInterface) {
    $context_plugins[] = $condition;
    }

    $contexts = $this->contextManager->getContexts($context_plugins);

    That nicely decouples implementation from Interface, it also removes the hardcoding for conditions.

larowlan’s picture

Assigned: Unassigned » larowlan
larowlan’s picture

Assigned: larowlan » Unassigned
Status: Needs work » Needs review
FileSize
6.7 KB
24.94 KB

Fixes unit tests, doesn't touch #58 or functional tests.
Out of time for today.
Time poor.

Status: Needs review » Needs work

The last submitted patch, 60: block-context-2354889.60.patch, failed testing.

EclipseGc’s picture

  1. +++ b/core/modules/block/src/BlockAccessControlHandler.php
    @@ -85,18 +92,20 @@ protected function checkAccess(EntityInterface $entity, $operation, $langcode, A
    +      if ($entity->getVisibilityConditions()) {
    

    Why? This should return empty array if there are no conditions, and foreach empty array still won't perform any of the subsequent actions. ??

  2. +++ b/core/modules/block/src/BlockRepositoryInterface.php
    @@ -19,6 +21,14 @@
    +   * @return array
    

    can we get a better type hint return here?

  3. +++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
    @@ -128,9 +135,24 @@ public function build() {
    +            $this->contextManager->getContextProvider($context_service_id)->onBlockActiveContext($block_event);
    

    This is going to invoke the same active context method multiple times if its mapped into multiple blocks. Is that what we want to do?

All in all this is a little weird. I get where we're trying to go, and I think why. I'll think on this a bit more and be back soon.

Eclipse

larowlan’s picture

Status: Needs work » Needs review
FileSize
1.24 KB
26.18 KB

More test fixes.
Out of time again.

I agree with points in 58 and 62, just haven't had time to implement yet.

Still to-do list:

Status: Needs review » Needs work

The last submitted patch, 63: block-context-2354889.63.patch, failed testing.

EclipseGc’s picture

Status: Needs work » Needs review
FileSize
10.95 KB

This is not really a competitive patch but some of the slow down that's been central to this problem has to do with (I think) when the typed data manager stuff gets called. Looking at this further, I was troubled by the implications of the public setters on the classes, so I attempted to remove them and rework Context classes as dumb data objects. The tagged context services that are ALSO event subscribers are just weird to me. I see what we're attempting to do with the @service_id, but it still seems odd to me. Just want to make sure we don't have other options. I don't expect this will pass, I have no clue about the performance.

Eclipse

Status: Needs review » Needs work

The last submitted patch, 65: 2354889-65.patch, failed testing.

Wim Leers’s picture

#65:

The tagged context services that are ALSO event subscribers are just weird to me.

+1

+++ b/core/modules/block/src/EventSubscriber/NodeRouteContext.php
@@ -40,16 +41,14 @@ public function __construct(RouteMatchInterface $route_match) {
-      $context = new Context(new ContextDefinition('entity:node'));
-      $context->setContextValue(Node::create(array('type' => $node_type->id())));
+      $context = new Context(new ContextDefinition('entity:node'), EntityAdapter::createFromEntity(Node::create(array('type' => $node_type->id()))));
...
     }

This would definitely be a big improvement — significantly less weird.

… but… isn't this out of scope for this particular issue? It seems an orthogonal problem to fix? (Then again, I don't understand the Context system well enough to say this with authority.)

dawehner’s picture

@EclipseGc
Opened up a dedicated issue for you, where you can improve the general speed: #2508884: Make contexts immutable
This issue is also about scalability

Wim Leers’s picture

#68: good call.

dawehner’s picture

just some normal review ... more later

  1. +++ b/core/modules/block/block.services.yml
    @@ -12,16 +12,29 @@ services:
    +  block.user_route_context:
    +    class: Drupal\block\EventSubscriber\UserRouteContext
    +    arguments: ['@current_route_match']
    +    tags:
    +      - { name: 'event_subscriber' }
    +      - { name: 'block_context_provider' }
    

    meh I'd expected it as part of user to be honest

  2. +++ b/core/modules/block/src/BlockAccessControlHandler.php
    @@ -8,6 +8,8 @@
    @@ -44,6 +46,11 @@ class BlockAccessControlHandler extends EntityAccessControlHandler implements En
    
    @@ -44,6 +46,11 @@ class BlockAccessControlHandler extends EntityAccessControlHandler implements En
       protected $contextHandler;
     
       /**
    +   * The global block contexts.
    +   */
    +  protected $contexts;
    +
    +  /**
        * {@inheritdoc}
        */
       public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
    @@ -85,18 +92,20 @@ protected function checkAccess(EntityInterface $entity, $operation, $langcode, A
    
    @@ -85,18 +92,20 @@ protected function checkAccess(EntityInterface $entity, $operation, $langcode, A
           return AccessResult::forbidden()->cacheUntilEntityChanges($entity);
         }
         else {
    -      $contexts = $entity->getContexts();
           $conditions = [];
    -      foreach ($entity->getVisibilityConditions() as $condition_id => $condition) {
    -        if ($condition instanceof ContextAwarePluginInterface) {
    -          try {
    -            $this->contextHandler->applyContextMapping($condition, $contexts);
    -          }
    -          catch (ContextException $e) {
    -            return AccessResult::forbidden()->setCacheMaxAge(0);
    +      if ($entity->getVisibilityConditions()) {
    +        foreach ($entity->getVisibilityConditions() as $condition_id => $condition) {
    +          if ($condition instanceof ContextAwarePluginInterface) {
    +            try {
    +              $contexts = $entity->getContexts();
    +              $this->contextHandler->applyContextMapping($condition, $contexts);
    +            }
    +            catch (ContextException $e) {
    +              return AccessResult::forbidden()->setCacheMaxAge(0);
    +            }
               }
    +          $conditions[$condition_id] = $condition;
             }
    -        $conditions[$condition_id] = $condition;
           }
           if ($this->resolveConditions($conditions, 'and') !== FALSE) {
    

    I'm a bit confused, $contexts seems to be never actually used here?

  3. +++ b/core/modules/block/src/BlockContextManager.php
    @@ -0,0 +1,40 @@
    +/**
    + * Provides a block context manager.
    + */
    +class BlockContextManager implements BlockContextManagerInterface {
    

    So far it doesn't manage anything, but rather just collects

dawehner’s picture

more nitpicking

+++ b/core/modules/block/src/BlockRepositoryInterface.php
@@ -19,6 +21,14 @@
+   * @return array

you can use \Drupal\block\BlockInterface[]

larowlan’s picture

Status: Needs work » Needs review
FileSize
1.87 KB
26.91 KB

more test fixes
still to do

Status: Needs review » Needs work

The last submitted patch, 72: block-context-2354889.71.patch, failed testing.

larowlan’s picture

Status: Needs work » Needs review
FileSize
3.51 KB
25.38 KB

Removes the 'user from route' context I'd only added for local testing/research sake in #34 (didn't mean for that to make it into the patch).

This should fix the last fail - comment_entity_storage_load is being during rebuild container (in DrupalKernel::initializeContainer we do a lot of work to carry through the current user, but it results in comment_entity_storage_load running before the container is fixed).

larowlan’s picture

still to do

Status: Needs review » Needs work

The last submitted patch, 74: block-context-2354889.74.patch, failed testing.

larowlan’s picture

Status: Needs work » Needs review
FileSize
741 bytes
25.38 KB

whoops

larowlan’s picture

Fixes #70 and #71 (parts of 70 were fixed in #74)

Still to do:

The last submitted patch, 72: block-context-2354889.71.patch, failed testing.

The last submitted patch, 74: block-context-2354889.74.patch, failed testing.

andypost’s picture

+++ b/core/modules/comment/comment.module
@@ -339,7 +339,7 @@ function comment_entity_storage_load($entities, $entity_type) {
-  if (!\Drupal::service('comment.manager')->getFields($entity_type)) {
+  if (!\Drupal::hasService('comment.manager') || !\Drupal::service('comment.manager')->getFields($entity_type)) {

how comment manager could be not registered if comment module enabled?

comment_entity_storage_load is being during rebuild container (in DrupalKernel::initializeContainer we do a lot of work to carry through the current user, but it results in comment_entity_storage_load running before the container is fixed).

please elaborate

catch’s picture

Do we need to fix #2495087: comment_entity_storage_load() is too expensive on cold caches and postpone this issue on that?

xjm’s picture

Issue tags: +D8 critical triage deferred

@catch, @alexpott, @effulgentsia, and I discussed this issue today. We decided to defer triage of this critical because the performance gains for core alone are not that significant, and it's unclear whether they will be that significant with contrib either, but the change does require a BC break that would potentially affect a number of significant contrib modules. We'll revisit this issue again in a week or two.

EclipseGc’s picture

That seems super sane to me, in the mean time could we look at #2508884: Make contexts immutable and see if it provides any performance gains since I was explicitly trying to move the most expensive operations in the existing code flow past the caching point? It'd be great to see if it succeeded at all and might put some of the changes proposed in this issue in a different light.

Thanks!

Eclipse

Wim Leers’s picture

I'm very surprised about #83. What about #24 and #25?

EDIT: Just providing a bit more explanation, showing some of the reasoning — would be appreciated.

dawehner’s picture

#83 maybe is fair in terms of performance, but from my point of view, this issue is mainly about scalability, which for example #2508884: Make contexts immutable doesn't solve.
Once contrib adds like kind of for contexts, like one for every entity type, things will blow up pretty quickly.

larowlan’s picture

Assigned: Unassigned » larowlan
larowlan’s picture

Fixes #62

Still to do:

More tomorrow

larowlan’s picture

#67 was a different approach, so just #58 to go, working on that now

larowlan’s picture

Starts on #58
Looking for feedback from bot and humans alike

The event and the base subscriber still remains, as I'm sure that is part of a bigger picture.

Figure there will be fails, will tackle them tomorrow

Status: Needs review » Needs work

The last submitted patch, 90: block-context-2354889.90.patch, failed testing.

larowlan’s picture

Status: Needs work » Needs review
FileSize
591 bytes
30.58 KB

missed an easy one

Status: Needs review » Needs work

The last submitted patch, 92: block-context-2354889.92.patch, failed testing.

larowlan’s picture

Status: Needs work » Needs review
FileSize
3.33 KB
32.73 KB

This should be last of feedback from bot, now for human feedback :)

Fabianx’s picture

Overall looks really great! Just some questions and remarks:

  1. +++ b/core/modules/block/src/BlockAccessControlHandler.php
    @@ -44,6 +46,11 @@ class BlockAccessControlHandler extends EntityAccessControlHandler implements En
       /**
    +   * The global block contexts.
    +   */
    +  protected $contexts;
    

    Unused

  2. +++ b/core/modules/block/src/BlockAccessControlHandler.php
    @@ -85,12 +92,11 @@ protected function checkAccess(EntityInterface $entity, $operation, $langcode, A
    -      $contexts = $entity->getContexts();
    ...
    -            $this->contextHandler->applyContextMapping($condition, $contexts);
    +            $this->contextHandler->applyContextMapping($condition, $entity->getContexts());
    

    Is that change necessary?

    Does it help performance?

    Would that not call getContexts() for each condition?

  3. +++ b/core/modules/block/src/BlockContextManager.php
    @@ -0,0 +1,64 @@
    +  public function getContexts(array $context_ids = NULL) {
    ...
    +        $this->container->get($id)->onBlockAdministrativeContext($event);
    ...
    +        $this->container->get($id)->onBlockActiveContext($event);
    
    +++ b/core/modules/block/src/BlockContextManagerInterface.php
    @@ -0,0 +1,26 @@
    +   * @param string[]|NULL $context_ids
    +   *   (optional) Context provider IDs to get contexts for. If none are passed
    +   *   gets all contexts. Should only be used in an administrative context.
    

    I think this should explain clearer when it returns what.

    However my suggestion is have two separate methods:

    - getContexts()
    - getAdministrativeContexts()

  4. +++ b/core/modules/block/src/BlockForm.php
    @@ -117,7 +124,7 @@ public function form(array $form, FormStateInterface $form_state) {
    -    $form_state->setTemporaryValue('gathered_contexts', $this->dispatcher->dispatch(BlockEvents::ADMINISTRATIVE_CONTEXT, new BlockContextEvent())->getContexts());
    +    $form_state->setTemporaryValue('gathered_contexts', $this->contextManager->getContexts(NULL));
    

    Would then be:

    ->getAdministrativeContexts()

    That would feel good.

  5. +++ b/core/modules/block/src/BlockRepository.php
    @@ -49,15 +49,21 @@ public function __construct(EntityManagerInterface $entity_manager, ThemeManager
    +  public function getVisibleBlocksPerRegion(array $blocks, array $contexts) {
    

    Necessary API change, but I think its the best way and there should not be that many users of that function in contrib.

    @todo Check in beta eval and IS.

  6. +++ b/core/modules/block/src/EventSubscriber/BlockContextSubscriberBase.php
    @@ -14,13 +15,12 @@
         $events[BlockEvents::ADMINISTRATIVE_CONTEXT][] = 'onBlockAdministrativeContext';
    

    Are we using that event still? (It did seem like BlockContextManager now took over that task).

  7. +++ b/core/modules/block/src/EventSubscriber/CurrentLanguageContext.php
    @@ -48,7 +48,7 @@ public function onBlockActiveContext(BlockContextEvent $event) {
    +        $event->setContext('language.' . $type_key . '@block.current_language_context', $context);
    

    We should document the service ID in the class doc block, as it means it cannot be changed - which I am fine with.

    Or we need to use $this->_serviceId or have an official API to ask the container for a services name. (follow-up though)

  8. +++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
    @@ -128,9 +135,29 @@ public function build() {
    +      foreach ($block->getVisibilityConditions() as $condition) {
    

    While this issue is specifically about the block context conditions, there is also another block context - IIRC.

    I think the blocks would have been able to use that one with the global event.

    So I _think_ we need to fix that case too to avoid any regressions.

  9. +++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
    @@ -128,9 +135,29 @@ public function build() {
    +            list(, $context_service_id) = explode('@', $context_provider_id);
    

    nit - simple hardening explode('@', $context_provider_id, 2);

larowlan’s picture

larowlan’s picture

larowlan
timplunkett: hi wanted to ask you about BlockEvent and page manager
7:35
timplunkett
yes
7:35
larowlan
timplunkett: https://www.drupal.org/node/2354889
7:35
Druplicon
https://www.drupal.org/node/2354889 => Make block context faster by removing onBlock event and replace it with loading from a BlockContextManager #2354889: Make block context faster by removing onBlock event and replace it with loading from a ContextManager => 95 comments, 18 IRC mentions
7:35
larowlan
timplunkett: in there moving from event subscribers to tagged services, block context provider manager
timplunkett: does page manager need them tagged as event subscribers/use the event system?
7:36
timplunkett
larowlan: page_manager has its own version of that code. haven't decided if we'll follow core's move
7:36
timplunkett
nope
7:36
larowlan
timplunkett: if not any dramas with removing the event|blocksubscriberbase and adding a new interface for context provider services?
timplunkett: does page manager require block? would the code be more page manager friendly if the manager wasn't part of block module
7:37
timplunkett
larowlan: http://cgit.drupalcode.org/page_manager/tree/src/EventSubscriber/CurrentUserContext.php
7:37
timplunkett
we have a whole other thing for this, because we have PageManagerContextEvent while core uses BlockContextEvent
7:37
timplunkett
larowlan: we need the block plugins, we don't need these contexts
7:38
larowlan
timplunkett: right so page manager could install without block?
7:38
timplunkett
well it'd be all but useless
we have a dependency, i don't know if it matters
7:38
larowlan
timplunkett: do you think it would make sense to try and align the two so there is one lot of code
7:39
timplunkett
larowlan: see https://www.drupal.org/node/2511568
7:39
Druplicon
https://www.drupal.org/node/2511568 => Create "context stack" service where available contexts can be registered #2511568: Create "context stack" service where available contexts can be registered => 0 comments, 2 IRC mentions
7:41
larowlan
timplunkett: sounds a lot like BlockContextProvider
timplunkett++
7:41
timplunkett
larowlan: right but has nothing to do with blocks
7:42
larowlan
timplunkett: yep, can make it more generic
timplunkett: but should it live in core or block module?
7:43
timplunkett
larowlan: would be cool if it was generic enough to be in Drupal\Core
7:43
plach [~plach@host-94-196-68-109.arq1.ldn.uk.sharedband.net] entered the room.
7:43
plach left the room (quit: Client Quit).
7:43
larowlan
I think that can be done
dawehner’s picture

honestly i'm not sure whether collecting all the services in a parameter is something we should do. This is kind of a new concept compared to just pushing in the service IDs using a pass directly. Any reason why you went this route?

  1. +++ b/core/modules/block/src/BlockAccessControlHandler.php
    @@ -8,6 +8,8 @@
     use Drupal\Component\Plugin\Exception\ContextException;
    +use Drupal\block\Event\BlockContextEvent;
    +use Drupal\block\Event\BlockEvents;
     use Drupal\Core\Access\AccessResult;
    

    Both new lines aren't needed.

  2. +++ b/core/modules/block/src/BlockAccessControlHandler.php
    @@ -44,6 +46,11 @@ class BlockAccessControlHandler extends EntityAccessControlHandler implements En
       /**
    +   * The global block contexts.
    +   */
    +  protected $contexts;
    +
    +  /**
    

    It would be great to explain a bit what are global block contexts and @var string could be added.

  3. +++ b/core/modules/block/src/BlockContextManager.php
    @@ -0,0 +1,64 @@
    +    if (!$context_ids) {
    +      foreach ($this->contextProviderIds as $id) {
    +        $this->container->get($id)->onBlockAdministrativeContext($event);
    +      }
    +    }
    +    else {
    +      foreach ($context_ids as $id) {
    +        $this->container->get($id)->onBlockActiveContext($event);
    +      }
    +    }
    

    This is honestly really really confusing code ... at least some documentation why specifying them externally are admin contexts and passing them in actual runtime contexts.

  4. +++ b/core/modules/block/src/BlockContextProviderInterface.php
    @@ -0,0 +1,33 @@
    +  /**
    +   * Gets the context for the active event.
    ...
    +  /**
    +   * Gets the context for the admin event.
    +   *
    

    Gets is not really the perfect description. What about Allows to determine context for the ... event

  5. +++ b/core/modules/block/src/EventSubscriber/BlockContextSubscriberBase.php
    @@ -14,13 +15,12 @@
        */
       public static function getSubscribedEvents() {
    -    $events[BlockEvents::ACTIVE_CONTEXT][] = 'onBlockActiveContext';
         $events[BlockEvents::ADMINISTRATIVE_CONTEXT][] = 'onBlockAdministrativeContext';
         return $events;
       }
    

    Yeah I guess we need a CR about that telling peopl ehow to write block contexts now.

  6. +++ b/core/modules/block/src/EventSubscriber/NodeRouteContext.php
    @@ -44,13 +44,13 @@ public function onBlockActiveContext(BlockContextEvent $event) {
    -      $event->setContext('node.node', $context);
    +      $event->setContext('node.node@block.node_route_context', $context);
    

    It would be also great to have somewhere, maybe the BlockContextProviderInterface the idea of having @ in name of the contexts.

  7. +++ b/core/modules/block/tests/src/Unit/Plugin/DisplayVariant/BlockPageVariantTest.php
    @@ -220,6 +225,9 @@ public function testBuildWithoutMainContent() {
    diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module
    
    diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module
    index 8bcac2c..47b9cb0 100644
    
    index 8bcac2c..47b9cb0 100644
    --- a/core/modules/comment/comment.module
    
    --- a/core/modules/comment/comment.module
    +++ b/core/modules/comment/comment.module
    
    +++ b/core/modules/comment/comment.module
    +++ b/core/modules/comment/comment.module
    @@ -339,7 +339,7 @@ function comment_entity_storage_load($entities, $entity_type) {
    
    @@ -339,7 +339,7 @@ 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)) {
    +  if (!\Drupal::hasService('comment.manager') || !\Drupal::service('comment.manager')->getFields($entity_type)) {
         // Do not query database when entity has no comment fields.
         return;
    

    This change feels a little bit optional to be honest. Do you mind quickly explaining why this is needed as part of this patch?

Berdir’s picture

Didn't really read any comments yet and not a full review, so just ignore what has been discussed already...

  1. +++ b/core/modules/block/src/BlockAccessControlHandler.php
    @@ -85,12 +92,11 @@ protected function checkAccess(EntityInterface $entity, $operation, $langcode, A
         }
         else {
    -      $contexts = $entity->getContexts();
           $conditions = [];
           foreach ($entity->getVisibilityConditions() as $condition_id => $condition) {
             if ($condition instanceof ContextAwarePluginInterface) {
               try {
    -            $this->contextHandler->applyContextMapping($condition, $contexts);
    +            $this->contextHandler->applyContextMapping($condition, $entity->getContexts());
               }
               catch (ContextException $e) {
    

    Maybe it wasn't before, but this really seems like an unnecessary change now? You'll also avoid a conflict with #2375695: Condition plugins should provide cache contexts AND cacheability metadata needs to be exposed here.

  2. +++ b/core/modules/block/src/BlockContextManager.php
    @@ -0,0 +1,64 @@
    +  public function __construct(ContainerInterface $container, array $contexts) {
    +    $this->container = $container;
    +    $this->contextProviderIds = $contexts;
    

    contexts vs contextProviderIds.. seems like that should be a bit more consistent?

    Don't we usually use the container aware thing for injection of the container?

  3. +++ b/core/modules/block/src/BlockContextManager.php
    @@ -0,0 +1,64 @@
    +   */
    +  public function getContexts(array $context_ids = NULL) {
    +    $event = new BlockContextEvent();
    +    if (!$context_ids) {
    +      foreach ($this->contextProviderIds as $id) {
    +        $this->container->get($id)->onBlockAdministrativeContext($event);
    +      }
    +    }
    +    else {
    +      foreach ($context_ids as $id) {
    +        $this->container->get($id)->onBlockActiveContext($event);
    +      }
    +    }
    

    Wouldn't it be easier to have two methods for this and one where $context_ids is rquired?

  4. +++ b/core/modules/block/src/BlockRepositoryInterface.php
    @@ -19,6 +21,14 @@
        */
    -  public function getVisibleBlocksPerRegion(array $contexts);
    +  public function getVisibleBlocksPerRegion(array $blocks, array $contexts);
    +
    

    order of the methods in the interface and the class is different, diff looks very weird, maybe move the new method below so it's reasier to review and cosistent with the interface?

  5. +++ b/core/modules/block/src/EventSubscriber/BlockContextSubscriberBase.php
    @@ -14,13 +15,12 @@
       /**
        * {@inheritdoc}
        */
       public static function getSubscribedEvents() {
    -    $events[BlockEvents::ACTIVE_CONTEXT][] = 'onBlockActiveContext';
         $events[BlockEvents::ADMINISTRATIVE_CONTEXT][] = 'onBlockAdministrativeContext';
         return $events;
       }
    

    seems like we can remove this completely now?

Fabianx’s picture

#98: Cache Contexts Manager uses that part of collecting things. It allows to avoid having to use proxies for all and allows knowing the service_ids, which as with cache contexts become important here now.

#99: I had all of that all in #95, so we are on the same page.

dawehner’s picture

#98: Cache Contexts Manager uses that part of collecting things. It allows to avoid having to use proxies for all and allows knowing the service_ids, which as with cache contexts become important here now.

Well right, but I don't get why we not just inject that into the service and be done. Storing the parameter, as it it would be some form of configuation, seems a bit redundant.

lauriii’s picture

There was quite a lot of conflicts with the latest head because of #2375695: Condition plugins should provide cache contexts AND cacheability metadata needs to be exposed so just a reroll to see how many web/kernel tests this breaks

Status: Needs review » Needs work

The last submitted patch, 102: make_block_context-2354889-102.patch, failed testing.

lauriii’s picture

Status: Needs work » Needs review
FileSize
31.34 KB
7.9 KB

I think I've addressed comments from #95, #98 and #99 and should have fixed the tests too.

Status: Needs review » Needs work

The last submitted patch, 104: make_block_context-2354889-104.patch, failed testing.

Fabianx’s picture

  1. +++ b/core/modules/block/src/BlockContextManager.php
    @@ -48,17 +48,18 @@
    -    if (!$context_ids) {
    -      foreach ($this->contexts as $id) {
    -        $this->container->get($id)->onBlockAdministrativeContext($event);
    -      }
    ...
    +  public function getAdministrativeContexts() {
    +    return $this->getContexts($this->contexts);
    +  }
    

    This is wrong.

    This needs to call onBlockAdministrativeContext() and can't use getContexts().

  2. +++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
    @@ -147,17 +148,15 @@
    +      $contexts += $block->getContexts();
    

    I don't think that will work as the block does not know its contexts.

    I _think_ (not sure will need a test) that this should parse the context IDs in the same way, then get them from the contextManager.

larowlan’s picture

picking this back up

larowlan’s picture

Status: Needs work » Needs review
FileSize
41.49 KB
51.18 KB

This:
* Moves everything to \Drupal\Core\Plugin\Context namespace so it is generic enough for page manager to use too
* Moved BlockContextManager to ContextStackManager
* Introduces ContextStack which is an amalgamation of ContextCollection objects
* Moves the block event to a ContextCollection - this is passed around for services to add contexts too
* The context stack manager takes care of setting the service ID on the context collection objects (so the individual providers no longer need to use the somecontext@service_id notation
* The context stack object takes care of fetching contexts from the context collection objects and converting their keys to use the @notation
* Gets rid of the event completely

This feels a lot less clunky in my book

dsnopek’s picture

Only skimmed through the patch quick on my cell phone, not a real review but this looks awesome! This will be great for Panels and probably Rules too.Thanks for hacking on this!

larowlan’s picture

The only clunky bit left here is BlockPageVariant needing to gather the service IDs with explode

larowlan’s picture

Removes the clunkiness of BlockPageVariant needing to know that the mappings contain @

larowlan’s picture

Assigned: larowlan » Unassigned

Un-assigning so others can work on it during sprint

larowlan’s picture

Issue tags: +needs profiling

We still need to profile against OP

And need to list API changes in OP

Fabianx’s picture

#111 looks truly fantastic, the interdiff also makes a lot of sense.

larowlan’s picture

Now that I think about it, I think ContextStackManager could be a plugin manager, with each of the context providers as plugins. Then the tagged services goes away in favour of Context Plugins and the service IDs become plugin IDs.

This is not dissimilar to context plugins in D7 ctools.

Should be simple from where we are now.

Thoughts?

The last submitted patch, 108: block-context-2354889-2.108.patch, failed testing.

larowlan’s picture

Fixes some tests

Fabianx’s picture

Title: Make block context faster by removing onBlock event and replace it with loading from a BlockContextManager » Make block context faster by removing onBlock event and replace it with loading from a ContextStackManager
Status: Needs review » Needs work
Issue tags: +Needs tests
  1. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextStack.php
    @@ -0,0 +1,52 @@
    +    /* @var \Drupal\Core\Plugin\Context\ContextCollectionInterface $context_stack */
    +    foreach ($collections as $context_stack) {
    +      foreach ($context_stack->getContexts() as $context_id => $context) {
    +        $this->contexts[$context_id . '@' . $context_stack->getServiceId()] = $context;
    

    s/context_stack/collection/g

  2. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextStackManager.php
    @@ -0,0 +1,79 @@
    +   * Constructs a CacheContextsManager object.
    

    ContextStackManager

  3. +++ b/core/modules/block/block.services.yml
    @@ -8,20 +8,20 @@ services:
    -      - { name: 'event_subscriber' }
    +      - { name: 'context_provider' }
    

    Love it!

  4. +++ b/core/modules/block/src/ContextProvider/CurrentLanguageContext.php
    @@ -54,7 +55,7 @@ public function onBlockActiveContext(BlockContextEvent $event) {
    +        $collection->setContext('language.' . $type_key, $context);
    

    Very nice!

  5. +++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
    @@ -129,10 +136,25 @@ public function build() {
    +      foreach ($block->getVisibilityConditions() as $condition) {
    

    This is missing the other contexts that can be on blocks, to e.g. variate by a given node.

    As the ACTIVE_CONTEXT had taken care of that, I think this would be a regression.

    => CNW

Looks fantastic, needs some tests and needs to deal with the non-condition contexts that can be on block plugins.

jibran’s picture

Now that I think about it, I think ContextStackManager could be a plugin manager, with each of the context providers as plugins. Then the tagged services goes away in favour of Context Plugins and the service IDs become plugin IDs.

This is a good idea but if this gives us the better performance then the current patch then we can switch to the plugin approach because performance is a major concern here.

The last submitted patch, 111: block-context-2354889-2.111.patch, failed testing.

The last submitted patch, 117: block-context-2354889-2.117.patch, failed testing.

larowlan’s picture

Status: Needs work » Needs review
FileSize
5.58 KB
51.29 KB

more test fixes

benjy’s picture

+++ b/core/lib/Drupal/Core/Plugin/Context/ContextStack.php
@@ -0,0 +1,52 @@
+class ContextStack implements ContextStackInterface {

It's confusing to me, but this class doesn't actually have the semantics of a stack?

Status: Needs review » Needs work

The last submitted patch, 122: block-context-2354889-2.122.patch, failed testing.

larowlan’s picture

Benjy, agree that's why I think plugin manager makes sense

dawehner’s picture

I like where we are heading towards to.

  1. +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/ContextProvidersPass.php
    @@ -0,0 +1,32 @@
    +
    +    $container->setParameter('context_providers', $context_providers);
    +  }
    

    I still think just injecting it would be fine.

  2. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextCollection.php
    @@ -0,0 +1,61 @@
    +   *
    +   * @var \Drupal\Core\Plugin\Context\Context[]
    +   */
    

    Can we use \Drupal\Core\Plugin\Context\ContextInterface here?

  3. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextCollectionInterface.php
    @@ -0,0 +1,54 @@
    + */
    +interface ContextCollectionInterface {
    +
    
    +++ b/core/lib/Drupal/Core/Plugin/Context/ContextProviderInterface.php
    @@ -0,0 +1,63 @@
    + */
    +interface ContextProviderInterface {
    +
    

    On an abstract level a context collection does not have direct dependencies, right? This could be a component and handle the CacheableDependencyInterface inside the core implementation.

    +10 to have an interface instead of the thrown event!

  4. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextStack.php
    @@ -0,0 +1,52 @@
    +<?php
    +/**
    + * @file
    + * Contains ContextStack.php
    + */
    

    Nitpick: Missing new line, wrong @file

  5. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextStack.php
    @@ -0,0 +1,52 @@
    +/**
    + * Defines a class for combining ContextCollection objects.
    + */
    +class ContextStack implements ContextStackInterface {
    

    I think we should explain here better, what the purposes are.

  6. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextStack.php
    @@ -0,0 +1,52 @@
    +  protected $contexts;
    

    Should we default to an empty array? I'd say so.

  7. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextStack.php
    @@ -0,0 +1,52 @@
    +        $this->contexts[$context_id . '@' . $context_stack->getServiceId()] = $context;
    ...
    +      $this->contexts[$context_id . '@' . $collection->getServiceId()] = $context;
    

    I was wondering whether defining such an idenfier could live on the ContextStackInterface and ContextCollectionInterface. So instead of having to hardcode that we have a service container go with something like $context_stack->getContextIdentifier($context_id)

  8. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextStackInterface.php
    @@ -0,0 +1,33 @@
    + */
    +interface ContextStackInterface {
    +
    

    Also a context stack seems to be a more generic concept

  9. +++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
    @@ -129,10 +136,25 @@ public function build() {
    +    $context_provider_ids = [];
    +    $contexts = [];
    +    foreach ($active_blocks as $block_id => $block) {
    +      /* @var \Drupal\block\BlockInterface $block */
    +      foreach ($block->getVisibilityConditions() as $condition) {
    +        if ($condition instanceof ContextAwarePluginInterface) {
    +          /* @var \Drupal\Core\Plugin\ContextAwarePluginInterface $condition */
    +          $context_provider_ids = array_unique(array_merge($context_provider_ids, $condition->getContextMapping()));
    +        }
    +      }
    +    }
    +    if ($context_provider_ids) {
    +      $contexts = $this->contextStackManager->getContexts($context_provider_ids);
    +    }
    +    $cacheable_metadata = [];
    

    do you mind moving this into its own protected method?

dawehner’s picture

Now that I think about it, I think ContextStackManager could be a plugin manager, with each of the context providers as plugins. Then the tagged services goes away in favour of Context Plugins and the service IDs become plugin IDs.

This is not dissimilar to context plugins in D7 ctools.

Should be simple from where we are now.

Thoughts?

Well, what would be the gain here really. Its just the different discovery you have here, and that is all. It though would be a bigger API change, as people would need to be move it to plugins itself. On top of that letting cache contexts and block contexts be similar at least in some way is not a bad thing developer experience wise.
It would be great if you could ellaborate the advantages of doing that.

For me personally this caused me a fried brain.

larowlan’s picture

For me the main advantage is if say contrib provides another 20 context providers, we don't end up with another 20 services in the container, instead just the plugin manager.

Then there's the DX and consistency with other plugins. Simpler to copy a file than add a service.

But yeah at the end of the day, its just a different discovery method.

larowlan’s picture

@FabianX

This is missing the other contexts that can be on blocks, to e.g. variate by a given node.

Can you elaborate? Are you saying I should be $contexts += $block->getContexts();?

Berdir’s picture

Not exactly sure what he means but $block->set/getContexts() is weird. HEAD is using that to transport the contexts around, BlockRepository currently sets them all and then BlockAccessControlHandler fetches them all again to apply them to the conditions. Note that this is the block entity and that has no concept of context awareness. That's something else entirely.

I'm fairly sure that this is a left-over of when visibility condition checking was part of BlockBase::access() since we needed them inside there then.

It would probably be very easy to clean up and just inject the new context manager into the BlockAccessControlHandler and fetch the contexts you need from there. You want to check that with @EclipseGc and how he is applying contexts to blocks exactly in his block context issue. Or check the code there.

dawehner’s picture

For me the main advantage is if say contrib provides another 20 context providers, we don't end up with another 20 services in the container, instead just the plugin manager.

Well, I still think its a separate discussion not needed to fix the critical issue.

dsnopek’s picture

A look at this again this morning. It's still awesome! :-)

One quick comment - can we move these out of the 'block' module:

  • \Drupal\block\ContextProvider\CurrentUserContext
  • \Drupal\block\ContextProvider\CurrentLanguageContext
  • \Drupal\block\ContextProvider\NodeRouteContext

I could definitely see these contexts being used by Rules, which has nothing to do with blocks.

xjm’s picture

Could we add a section to the issue summary here about the data model changes and the update that would be needed? Hopefully the service changes would no longer be a problem following #2507509: Service changes should not result in fatal errors between patch or minor releases.

Since this is a critical, the update hook and tests can be moved to a critical followup issue, but it would be good to know what they are before it goes in. Thanks!

Fabianx’s picture

#131: I agree, while I think plugin managers might make sense, because unlike cache contexts, I don't think contexts need to work on a low bootstrap level, this is fine as follow-up. Also the context parameter is fine for now - maybe something else needs it.

#130:

What my concern is:

I have a block that gets the node from the [route] to display an image field of said node. ( I think we have at least unit tests doing that.)

What I _think_ has happened before.

The onBlockActiveEvent was fired unconditionally, the Node Context was injected into the block via setContexts(), now the block is rendered with the node correctly.

Because the current code only checks for conditions on the block, so any other 'contexts' are not fulfilled anymore.

However it might be that a) this is already broken or b) this is not a concern or c) we should just put the whole discussion to a follow-up.

I am mainly the voice of caution here, though I personally won't mind a regression (if any) in this space, because if what we have now does not break tests, it is clearly lacking test coverage and things without test coverage we can IMHO temporarily break for a critical as thats like the network cord that does not have a label, so it must be pulled ...

dsnopek’s picture

Now that I think about it, I think ContextStackManager could be a plugin manager, with each of the context providers as plugins. Then the tagged services goes away in favour of Context Plugins and the service IDs become plugin IDs.

+1 to this for all the reasons given above. I think the amount of contrib this breaks would be super minimal because contrib has in many cases been forced to duplicate the context providers (this patch even with services would make that no longer necessary).

Fabianx’s picture

Discussed #134 in IRC, there is a method called getContextMapping(), but currently nothing in Core calls it, so likely context mapping for anything else than conditions is broken anyway (in core) ...

So lets get this in.

I think ContextStackManager could be renamed to a context plugin manager even if the discovery is hardcoded.

I use a ContainerAwarePluginManager in D7 with service_container module a lot and it works pretty great.

Then we would still have tagged services, but already a plugin manager, which would ease transition to real plugins later.

I would however also be okay to mark ContextStack as @internal for now and just follow-up on the critical.

Wim Leers’s picture

Well, I still think its a separate discussion not needed to fix the critical issue.

+1

I don't think contexts need to work on a low bootstrap level, this is fine as follow-up.

+1 — but… we should very very carefully evaluate whether plugins truly make more sense than services.

(We really need good documentation explaining the pros/cons of plugins vs. services, and the recommended criteria to choose either, because many contrib modules will have to choose either too!)


I apologize in advance for what will likely be an annoying review to address. But I find many parts here very confusing. Hopefully you'll find my suggestions/questions helpful to make this patch more understandable.

I think an explanation in the IS of the proposed architecture would massively help in understanding this patch. But the "Needs issue summary update" tag is already present.

  1. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextCollection.php
    @@ -0,0 +1,61 @@
    + * Contains \Drupal\Core\Plugin\Context\ContextCollection.
    ...
    +   * The service which provided the contexts.
    

    This seems like a contradiction? (service vs. plugin)

  2. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextCollection.php
    @@ -0,0 +1,61 @@
    + * Collects global active contexts for a given service.
    

    What does "global active" mean?

    "given service" -> what kind of service?

  3. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextCollection.php
    @@ -0,0 +1,61 @@
    +   * The array of available contexts.
    

    s/array/set/ ?

    "[…] collected from all context provider services"

  4. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextCollection.php
    @@ -0,0 +1,61 @@
    +   * Constructs a new context stack object.
    

    s/stack/collection/

  5. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextProviderInterface.php
    @@ -0,0 +1,63 @@
    +   * Determines the available run-time contexts.
    ...
    +  public function getActiveContexts(ContextCollection $collection);
    

    "run-time" vs. "active".

    I think "run-time" is much clearer. Can't we name the method accordingly?

  6. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextProviderInterface.php
    @@ -0,0 +1,63 @@
    +   * Determines the available configuration-time contexts.
    ...
    +  public function getAdministrativeContexts(ContextCollection $collection);
    

    Same here with "configuration-time" versus "administrative". Configuration-time is much clearer.

  7. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextStack.php
    @@ -0,0 +1,52 @@
    +/**
    + * Defines a class for combining ContextCollection objects.
    + */
    +class ContextStack implements ContextStackInterface {
    ...
    +        $this->contexts[$context_id . '@' . $context_stack->getServiceId()] = $context;
    ...
    +  /**
    +   * {@inheritdoc}
    +   */
    +  public function getContexts() {
    +    return $this->contexts;
    +  }
    

    This doesn't seem to be a stack data structure at all?

  8. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextStackInterface.php
    @@ -0,0 +1,33 @@
    +interface ContextStackInterface {
    +
    +  /**
    +   * Gets the active context objects.
    

    So context stacks contain context collections, and those collections apparently only contain active (== run-time) context? Then shouldn't ContextCollection be called ActiveContextCollection?

  9. +++ b/core/modules/block/src/BlockForm.php
    @@ -69,23 +70,30 @@ class BlockForm extends EntityForm {
    +   * The context manager service.
    ...
    +   * @var \Drupal\Core\Plugin\Context\ContextStackManagerInterface
    ...
    +  protected $contextStackManager;
    ...
    +   * @param \Drupal\Core\Plugin\Context\ContextStackManagerInterface $context_manager
    +   *   The block context manager service.
    

    Confusingly inconsistent.

  10. +++ b/core/modules/block/src/BlockRepositoryInterface.php
    similarity index 73%
    rename from core/modules/block/src/EventSubscriber/CurrentLanguageContext.php
    
    rename from core/modules/block/src/EventSubscriber/CurrentLanguageContext.php
    rename to core/modules/block/src/ContextProvider/CurrentLanguageContext.php
    
    +++ b/core/modules/block/src/ContextProvider/CurrentLanguageContext.php
    similarity index 73%
    rename from core/modules/block/src/EventSubscriber/CurrentUserContext.php
    
    rename from core/modules/block/src/EventSubscriber/CurrentUserContext.php
    rename to core/modules/block/src/ContextProvider/CurrentUserContext.php
    
    +++ b/core/modules/block/src/ContextProvider/CurrentUserContext.php
    similarity index 72%
    rename from core/modules/block/src/EventSubscriber/NodeRouteContext.php
    
    rename from core/modules/block/src/EventSubscriber/NodeRouteContext.php
    rename to core/modules/block/src/ContextProvider/NodeRouteContext.php
    

    These all look wonderful :)

Fabianx’s picture

#137: A plugin manager is just an interface for discovering services / plugins.

The underlying architecture does not matter as long as there are possibilities to get instances of the service / plugin.

The main difference between services and plugins is:

- Services are instantiated once and remain for the life time of the Container. (unless the deprecated "scope: prototype" is used - yes, that exists ...)
- Plugins are instantiated newly whenever instances of them are created.

When one wants to expose services as plugins, one can use:

  return clone $this->container->get($serviceId);

or explicitly define that this plugin manager always returns the same instances (singleton plugin manager), which is fine as long as there is not any configuration that is needed to be injected.

I personally like the plugin manager interface concept and given the ContextStackManager is newly introduced, that could be fine to use the correct interface for future proof-ness.

However l checked and changing the interface of getContexts() and getAdministrativeContexts() would not make sense.

The main thing changing would be that instead of $contexts being injected as a string a ContextPluginManager would be injected and

getAdministrativeContexts() would then do:

foreach ($this->contextPluginManager->getDefinitions() as $plugin_id => $definition) {
  $context_plugin = $this->contextPluginManager->createInstance($plugin_id); // instead of container->get(...)
  $active_contexts = $context_plugin->getAdministrativeContexts();
}

and

getContexts() would then do:

foreach ($context_ids as $plugin_id ) {
  $context_plugin = $this->contextPluginManager->createInstance($plugin_id); // instead of container->get(...)
  $active_contexts = $context_plugin->getActiveContexts();
}

So I don't think at this stage we would gain anything, but more complexity and as the container->get() is internally to the ContextStackManager that should definitely be follow-up.

Also: In any case - any move away from services is certainly follow-up.

Berdir’s picture

+++ b/core/lib/Drupal/Core/Plugin/Context/ContextStackManager.php
@@ -0,0 +1,79 @@
+  public function getContexts(array $context_ids) {
+    $stack = new ContextStack([]);
+    foreach ($context_ids as $id) {
+      // The IDs have been passed in {context_slot_name}@{service_id} format.
+      if (strpos($id, '@') !== FALSE) {
+        list(, $id) = explode('@', $id, 2);
+      }
+      $collection = new ContextCollection($id);
+      $this->container->get($id)->getActiveContexts($collection);
+      $stack->addContextCollection($collection);
+    }
+
+    return $stack->getContexts();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getAdministrativeContexts() {
+    $stack = new ContextStack([]);
+    foreach ($this->contexts as $id) {
+      $collection = new ContextCollection($id);
+      $this->container->get($id)->getAdministrativeContexts($collection);
+      $stack->addContextCollection($collection);
+    }
+
+    return $stack->getContexts();
+  }

There are a number of things here that don't really make sense to me yet.

* This still gets the *all* the contexts of a given service if any is requested. We should pass the/a list of slot_names (is that an official name now?) Everywhere else we seem to use context_id.
* I think we should also really statically cache the fetched contexts. They're not going to change during the request and it seems like unnecessary overhead to ask the same service multiple times for the same context.
* I don't really get why we need a stack *and* collections especially since both are just used internally. When combining with the things above, it will also get a lot more complex, because you need to add lots of methods to check if a collection exists in a the stack and get it, or alternatively get a specific context based on service_id and context_id. Seems at least the stack could be left out for now and we'd just use two arrays: $this->loadedContexts and $contexts = [] to return a list of contexts?
* We also need better names for whatever we're going to keep using. Both stack and collection are very generic concepts, but are used in a specific way here. Especially stack is confusing as others said, since I don't see how it is a stack implementation :)

pfrenssen’s picture

Status: Needs work » Needs review
Issue tags: +D8 Accelerate London
FileSize
57.73 KB
7.86 KB

Familiarizing myself with the issue as part of the D8 Accelerate sprint, cleaned up the documentation and use statements a bit while reading through the patch.

Fabianx’s picture

Status: Needs review » Needs work

The last submitted patch, 140: 2354889-140.patch, failed testing.

jibran’s picture

larowlan’s picture

Assigned: Unassigned » larowlan

prodding some more

larowlan’s picture

Status: Needs work » Needs review
FileSize
37.41 KB
54.01 KB

Still need an issue summary update and profiling.

Tries to address some of the reviews and clarify the naming.

Stack => Collection
Collection => Result
StackManager => Manager

Adds back the comment.module change, but I think we'll need to make the services lazy in order to revert that.

Status: Needs review » Needs work

The last submitted patch, 145: block-context-2354889-2.145.patch, failed testing.

larowlan’s picture

Status: Needs work » Needs review
FileSize
3.9 KB
53.98 KB

missed some spots

dawehner’s picture

I like the new naming much better!

  1. +++ b/core/core.services.yml
    @@ -1394,7 +1394,9 @@ services:
    +  context.stack_manager:
    

    Do we want to change the service ID as well?

  2. +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/ContextProvidersPass.php
    @@ -0,0 +1,32 @@
    +
    +    $container->setParameter('context_providers', $context_providers);
    +  }
    

    I'll just stop complaining about this.

  3. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextManager.php
    @@ -0,0 +1,77 @@
    +      $result = new ContextResult($id);
    +      $this->container->get($id)->getRunTimeContexts($result);
    

    API question, is there a result why getRuntimeContexts cannot just return the $result?

  4. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextManager.php
    @@ -0,0 +1,77 @@
    +    return $collection->getContexts();
    ...
    +    return $collection->getContexts();
    

    I'm curious whether there would be an advantage of return the collection instead of an array, we have things like RouteCollection in core

  5. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextManager.php
    @@ -0,0 +1,77 @@
    +  public function getConfigurationTimeContexts() {
    

    <3

  6. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextProviderInterface.php
    @@ -0,0 +1,63 @@
    + * Defines an interface for contexts that service contexts.
    

    Do you mind removing service out of that sentence? It let's my brain go into circles

  7. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextProviderInterface.php
    @@ -0,0 +1,63 @@
    +   * @param \Drupal\Core\Plugin\Context\ContextResult $result
    +   *   The result to which to add the available contexts.
    +   */
    +  public function getRunTimeContexts(ContextResult $result);
    

    Yeah let's look at the function signature again ... it does not get, it rather allows you to set it.

  8. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextResult.php
    @@ -0,0 +1,61 @@
    +/**
    + * Collects evaluated run-time contexts.
    + */
    +class ContextResult implements ContextResultInterface {
    

    That documentation is a little bit confusing, to be honest. To be honest I don't get why we can't just have a collection and put everything on there directly. The code calling out to all providers could deal with getServiceId() couldn't it? So provideRuntimeContexts() and maybe an array?

  9. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextResult.php
    @@ -0,0 +1,61 @@
    +  /**
    +   * {@inheritdoc}
    +   */
    +  public function getServiceId() {
    +    return $this->serviceId;
    +  }
    +
    

    As mentioned in earlier reviews, I think getServiceId() is a wrong coupling

  10. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextResultCollection.php
    @@ -0,0 +1,55 @@
    +  public function __construct(array $collections) {
    

    yeah that var name is wrong now

dawehner’s picture

Also did some quick profiling: Anonymous frontpage without page cache

### FINAL REPORT

=== 8.0.x..8.0.x compared (5598dda93304e..5598ddc4d4767):

ct  : 27,450|27,450|0|0.0%
wt  : 63,302|63,151|-151|-0.2%
mu  : 14,810,824|14,810,864|40|0.0%
pmu : 14,876,288|14,876,392|104|0.0%

=== 8.0.x..2354889 compared (5598dda93304e..5598ddd45e9cc):

ct  : 27,450|26,915|-535|-1.9%
wt  : 63,302|61,198|-2,104|-3.3%
mu  : 14,810,824|14,086,792|-724,032|-4.9%
pmu : 14,876,288|14,147,760|-728,528|-4.9%

---
ct = function calls, wt = wall time, cpu = cpu time used, mu = memory usage, pmu = peak memory usage

### XHPROF-LIB REPORT

+-------------------+------------+------------+------------+------------+------------+
| namespace         |        min |        max |       mean |     median |       95th |
+-------------------+------------+------------+------------+------------+------------+
| Calls             |            |            |            |            |            |
|                   |            |            |            |            |            |
| 2354889           |     26,915 |     28,602 |     26,932 |     26,915 |     26,915 |
| 8_0_x             |     27,450 |     29,280 |     27,468 |     27,450 |     27,450 |
|                   |            |            |            |            |            |
| Wall time         |            |            |            |            |            |
|                   |            |            |            |            |            |
| 2354889           |     61,198 |     85,004 |     64,100 |     63,518 |     69,855 |
| 8_0_x             |     63,151 |     88,114 |     66,393 |     65,547 |     72,402 |
|                   |            |            |            |            |            |
| Memory usage      |            |            |            |            |            |
|                   |            |            |            |            |            |
| 2354889           | 14,086,792 | 14,497,240 | 14,132,353 | 14,086,792 | 14,394,645 |
| 8_0_x             | 14,810,864 | 15,221,848 | 14,843,318 | 14,810,896 | 15,163,664 |
|                   |            |            |            |            |            |
| Peak memory usage |            |            |            |            |            |
|                   |            |            |            |            |            |
| 2354889           | 14,147,760 | 14,623,632 | 14,194,116 | 14,147,760 | 14,454,792 |
| 8_0_x             | 14,876,392 | 15,351,128 | 14,909,661 | 14,876,424 | 15,228,128 |
|                   |            |            |            |            |            |
+-------------------+------------+------------+------------+------------+------------+
larowlan’s picture

Assigned: larowlan » Unassigned

Thanks @dawehner - great feedback
Unassigning in case someone wants to work on it at the sprint
If not, will pick it up in the morning

lauriii’s picture

#148.2: I made it collect the context providers inside ContextManager since context providers are not needed else where.

#148.3-4: I don't really have answers to there but it might help to hear of the use cases where the object would be needed.

#148.8: Can you open up that a little bit more?

Fabianx’s picture

Status: Needs review » Needs work
+++ b/core/lib/Drupal/Core/Plugin/Context/ContextManager.php
@@ -76,2 +75,18 @@
+  protected function getContexts() {
+    if (!$this->contexts) {
+      foreach (array_keys($container->findTaggedServiceIds('context_provider')) as $id) {
+        $this->contexts[] = $id;
+      }
+    }
+
+    return $this->contexts;
+  }

This is not the right way and should not be done on run time anyway.

It would theoretically work now, because tests are usually run with a ContainerBuilder, but findTaggedServiceIds() is not part of the IntrospectableContainerInterface.

The proper way is to leave the compiler pass, but to remove the %context_ids and instead use:

$definition = $container->getDefinition('context.manager');
$definition->setArguments(array_merge($definition->getArguments()), [$context_provider_ids]);

or something like that.

I personally don't think the parameter is wrong, but that is the proper way.

The last submitted patch, 151: make_block_context-2354889-151.patch, failed testing.

lauriii’s picture

Status: Needs work » Needs review
FileSize
60.5 KB
3.91 KB

Didn't know that is possible, thanks!

Status: Needs review » Needs work

The last submitted patch, 154: make_block_context-2354889-153.patch, failed testing.

Fabianx’s picture

Uhm,

That did not work ...

But this should (see RegisterLazyRouteEnhancers):

    $container
      ->getDefinition('context.manager')
      ->addArgument($context_ids);
dawehner’s picture

Assigned: Unassigned » dawehner

Working on it ...

dawehner’s picture

Status: Needs work » Needs review
FileSize
54.21 KB
910 bytes

Uploading a less failing one for now.

dawehner’s picture

Making good simplification progress, stay tuned!

fago’s picture

Can you elaborate on the new concepts introduced? What's a ContextResult? Sounds like a general class for Contexts, or is it tailored to some specific use?

Same for ContextStackManager. Is its purpose registration of global context in general or is it tied to blocks?

Fabianx’s picture

#160: Registration of global contexts in general.

The main idea besides all the rest is that contexts are allowed to be transformed to:

- context_name@[serviceId]

so that the provider is saved explicitly and such can be called explicitly - as it was chosen by the user explictly on the UI. (or implictly if there is only one).

So:

On UI:

$this->context_manager->getAdministrativeContexts() returns all the contexts as the event before, but does encapsulate that in a manager instead of in a generic event.

Then the context to be used is stored in that format, so context_name@[serviceId].

On block loading time, the exact known context can be asked to set the context then instead of asking who can fulfill that, so all we need to do is call:

$this->context_manager->getContexts($context_ids);

and because the format is context_name@[serviceId], the context can be gotten from the container directly.

So it is absolutely generic to plugins, but blocks use it in this specific way.

The main reason for the added complexity is that contexts by definition are not bound to a service (but can be fulfilled by anything), so they need be transformed out and back and that is also what dawehner probably is simplifying.

The main reason (what took you so long?) is that the context system is very hard to understand for anyone not Tim or Eclipse or those working with panels or rules directly (likely).

@fago: So if you have experiences and can share documentation on how it works from your understanding (or how you use it in rules) that would help, too.

larowlan’s picture

Ready to take the baton back if you're about for a handover

dawehner’s picture

Assigned: dawehner » Unassigned
FileSize
55.65 KB
24.85 KB

Let's try.

larowlan’s picture

Assigned: Unassigned » larowlan

tag

Status: Needs review » Needs work

The last submitted patch, 163: 2354889-160.patch, failed testing.

larowlan’s picture

Status: Needs work » Needs review
FileSize
9.81 KB
261.17 KB

More cleanup
Another test
Removes ContextCollection as we don't need it anymore either

larowlan’s picture

FileSize
53.81 KB

forgot to merge

The last submitted patch, 166: block-context-2354889-2.166.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 167: block-context-2354889-2.167.patch, failed testing.

larowlan’s picture

Status: Needs work » Needs review
FileSize
750 bytes
53.8 KB

So if BlockPageVariant fatals, turns out lots of things break :)

Status: Needs review » Needs work

The last submitted patch, 170: block-context-2354889-2.168.patch, failed testing.

Wim Leers’s picture

This is looking much better already. It's now possible to get a sense of how all this works without already being very experienced with the context system — that's a great sign :)

In this review, a whole bunch of nitpicks, but also another batch of questions/unclarities that can be addressed by better consistency, more/better documentation and potentially by better naming.

  1. +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/ContextProvidersPass.php
    @@ -0,0 +1,33 @@
    + * Adds the context providers into the context manager.
    ...
    +   * Collects the context providers into the context_providers parameter.
    

    Nit: s/into/to/ ?

  2. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextManager.php
    @@ -0,0 +1,98 @@
    +/**
    + *
    

    Nit: extraneous \n.

  3. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextManager.php
    @@ -0,0 +1,98 @@
    +   * An array of available context providers service IDs.
    ...
    +   *   An array of the available cache context service IDs.
    

    Nit: we usually just say "The things" not "The array of things". Alternatively: "The set of […]" or "The list of […]". I think this is a set?

  4. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextManager.php
    @@ -0,0 +1,98 @@
    +   *   Context provider IDs.
    ...
    +  protected $contextServiceIDs = [];
    ...
    +  public function __construct(ContainerInterface $container, array $context_service_ids) {
    

    Inconsistent.

  5. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextManager.php
    @@ -0,0 +1,98 @@
    +    // First collect what context slots are needed by which context provider.
    

    First collect which contexts slots need which context providers.

    Better:

    Create a map of context providers (service IDs) to context slot names.

  6. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextManager.php
    @@ -0,0 +1,98 @@
    +    // Call to them and add the results to the context collection.
    

    s/to them/them/

    Why "collection"? It's simply an array.

    Better:

    Iterate over all context providers (services), gather the run time contexts and assign them to the slots that need them.

  7. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextManagerInterface.php
    @@ -0,0 +1,35 @@
    + * Manages available contexts and collects context values on runtime.
    

    s/on/at/

  8. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextManagerInterface.php
    @@ -0,0 +1,35 @@
    +   * Gets run time context provider objects for given context IDs.
    ...
    +   *   The runtime contexts.
    

    "run time" vs. "runtime"

    Elsewhere: "run-time".

    Needs tidying up.

  9. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextManagerInterface.php
    @@ -0,0 +1,35 @@
    +   * Gets all context provider objects for the purposes of configuration.
    

    Inconsistent with the docs of getRunTimeContexts().

  10. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextManagerInterface.php
    @@ -0,0 +1,35 @@
    +   *   The runtime contexts.
    

    s/runtime/configuration time/

  11. +++ b/core/modules/block/src/BlockForm.php
    @@ -69,23 +69,30 @@ class BlockForm extends EntityForm {
    +   *   The block context manager service.
    

    s/block//

  12. +++ b/core/modules/block/src/BlockRepositoryInterface.php
    @@ -22,6 +24,14 @@
    -  public function getVisibleBlocksPerRegion(array $contexts, array &$cacheable_metadata = []);
    +  public function getVisibleBlocksPerRegion(array $blocks, array $contexts, array &$cacheable_metadata = []);
    +
    +  /**
    +   * Gets the active blocks as block entities for the current theme.
    +   *
    +   * @return \Drupal\block\BlockInterface[]
    +   *   Array of block entities keyed by block ID.
    +   */
    +  public function getActiveBlocks();
    

    Interesting that both the existing method has to change and a new method is necessary.

  13. +++ b/core/modules/block/src/ContextProvider/CurrentLanguageContext.php
    @@ -41,10 +42,20 @@ public function __construct(LanguageManagerInterface $language_manager) {
    -  public function onBlockActiveContext(BlockContextEvent $event) {
    +  public function getRunTimeContexts(array $context_slot_names) {
    

    No longer coupled to blocks, and a hundred times clearer what it actually means ("active" is so vague in comparison) — yay!

  14. +++ b/core/modules/block/src/ContextProvider/CurrentLanguageContext.php
    @@ -41,10 +42,20 @@ public function __construct(LanguageManagerInterface $language_manager) {
    +    if ($context_slot_names) {
    +      foreach ($context_slot_names as $context_slot_name) {
    +        if (array_search(str_replace('language.', '', $context_slot_name), $language_types) === FALSE) {
    +          unset($language_types[str_replace('language.', '', $context_slot_name)]);
    +        }
    +      }
    +    }
    

    I have no idea what this is doing.

  15. +++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
    @@ -189,13 +194,30 @@ public function build() {
    +   * Retrieves the required contexts for active blocks.
    

    I almost wonder how one can know which blocks are active if we're still retrieving their required contexts? It sounds like a chicken-egg situation?

  16. +++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
    @@ -189,13 +194,30 @@ public function build() {
    +   *   Currently active blocks.
    

    What is the meaning of "currently" here? Can during the same request a different set of blocks become active?

  17. +++ b/core/modules/block/tests/src/Unit/BlockFormTest.php
    @@ -60,6 +53,14 @@ class BlockFormTest extends UnitTestCase {
    +  protected $contextManager;
    +
    +
    +  /**
    

    Nit: 2 \n, should be 1.

  18. +++ b/core/tests/Drupal/Tests/Core/Plugin/Context/ContextManagerTest.php
    @@ -0,0 +1,112 @@
    +   * Setups contexts and context providers.
    

    Nit: s/Setups/Sets up/

  19. +++ b/core/tests/Drupal/Tests/Core/Plugin/Context/ContextManagerTest.php
    @@ -0,0 +1,112 @@
    +   *   An array of setup contexts.
    

    Nit: s/setup/set up/

larowlan’s picture

172:

  1. Fixed
  2. Fixed
  3. Fixed
  4. Fixed
  5. Fixed
  6. Fixed
  7. Fixed
  8. Fixed
  9. Fixed
  10. Fixed
  11. Fixed
  12. Refactored so that placed blocks were stored in a class property to avoid needing to pass them back
  13. Ta
  14. The slot names include language., we're removing them
  15. Added comments/docs to address
  16. Clarified
  17. Fixed
  18. Fixed
  19. Fixed

Fixed the fail, thanks to Wim who pointed out that the NodeRouteContext was sometimes not returning a context, but it should always return something, even if it is an empty context value. Thanks again to Wim, please add him to commit credits.

Wim Leers’s picture

Awesome — thanks for the swift reroll, @larowlan! IMHO this patch is ready for new review rounds from others (@lauriii, @dawehner, @Fabianx, @Berdir etc.).

  1. +++ b/core/modules/block/src/BlockRepository.php
    @@ -32,6 +32,12 @@ class BlockRepository implements BlockRepositoryInterface {
       /**
    +   * @var array
    +   *   The set of active blocks for each theme.
    +   */
    

    Nit: wrong docblock formatting.

  2. +++ b/core/modules/block/src/BlockRepository.php
    @@ -50,8 +56,12 @@ public function __construct(EntityManagerInterface $entity_manager, ThemeManager
    +    if (!isset($this->blocksForTheme[$active_theme->getName()])) {
    +      $this->blocksForTheme[$active_theme->getName()] = $this->getBlocksForTheme();
    +    }
    

    So given that this existing public API function calls the new getBlocksForTheme()… I wonder if that new method needs to be a public API at all?

    IDK yet, just wondering out loud.

  3. +++ b/core/modules/block/src/BlockRepositoryInterface.php
    @@ -24,7 +22,7 @@
    -  public function getVisibleBlocksPerRegion(array $blocks, array $contexts, array &$cacheable_metadata = []);
    +  public function getVisibleBlocksPerRegion(array $contexts, array &$cacheable_metadata = []);
    

    Less API change, yay!

  4. +++ b/core/modules/block/src/ContextProvider/NodeRouteContext.php
    @@ -47,16 +47,15 @@ public function getRunTimeContexts(array $context_slot_names) {
    -      $result['node.node'] = $context;
         }
         elseif ($this->routeMatch->getRouteName() == 'node.add') {
           $node_type = $this->routeMatch->getParameter('node_type');
           $context->setContextValue(Node::create(array('type' => $node_type->id())));
    -      $result['node.node'] = $context;
         }
         $cacheability = new CacheableMetadata();
         $cacheability->setCacheContexts(['route']);
         $context->addCacheableDependency($cacheability);
    +    $result['node.node'] = $context;
    

    This is the bit that @larowlan mentioned at the end of his comment.

  5. +++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
    @@ -194,18 +194,23 @@ public function build() {
    +  protected function getActiveContexts(array $theme_blocks) {
    

    s/Active/RunTime/ ?

jibran’s picture

Title: Make block context faster by removing onBlock event and replace it with loading from a ContextStackManager » Make block context faster by removing onBlock event and replace it with loading from a ContextManager
Issue tags: -Needs tests +Needs change record

I don't think we need more tests here but we do need a change notice.

larowlan’s picture

FileSize
1.82 KB
53.28 KB

Fixes 174.1 and 5.
174.2 its called by BlockPageVariant, so needs to be public.

tim.plunkett’s picture

+++ b/core/modules/block/src/BlockRepository.php
@@ -52,13 +59,18 @@ public function __construct(EntityManagerInterface $entity_manager, ThemeManager
+    if (!isset($this->blocksForTheme[$active_theme->getName()])) {
+      $this->blocksForTheme[$active_theme->getName()] = $this->getBlocksForTheme();
+    }

@@ -84,4 +96,13 @@ public function getVisibleBlocksPerRegion(array $contexts, array &$cacheable_met
+    $active_theme = $this->themeManager->getActiveTheme();
+    $this->blocksForTheme[$active_theme->getName()] = $this->blockStorage->loadByProperties(['theme' => $active_theme->getName()]);
+    return $this->blocksForTheme[$active_theme->getName()];

Can we move the !isset check into the method?

larowlan’s picture

FileSize
1.48 KB
53.18 KB

sure

larowlan’s picture

Issue summary: View changes

Issue summary update including API changes

larowlan’s picture

Added profiling and data model changes to issue summary

larowlan’s picture

Issue summary: View changes
tim.plunkett’s picture

+++ b/core/modules/block/src/BlockRepository.php
@@ -52,13 +59,15 @@ public function __construct(EntityManagerInterface $entity_manager, ThemeManager
     $active_theme = $this->themeManager->getActiveTheme();
+    $blocks = $this->getBlocksForTheme();
...
+    foreach ($blocks as $block_id => $block) {

@@ -84,4 +93,15 @@ public function getVisibleBlocksPerRegion(array $contexts, array &$cacheable_met
+  public function getBlocksForTheme() {
+    $active_theme = $this->themeManager->getActiveTheme();

Hmm, sorry to nitpick this method again, but I think either we should pass in the $active_theme, or it should be renamed getBlocksForActiveTheme(). I think the first option makes more sense.

Also moving it back closer to the actual loop (or even foreach ($this->getBlocksForTheme($active_theme) as $block_id => $block) {) would keep things more logically grouped.

dawehner’s picture

Working on it for a while.

Fabianx’s picture

Because we do an API change here, we should also move out the conditions and condition service definitions out of block in Drupal\Core\ and core/core.services.yml - else this would need to be another API change and also a data model change (all IDs would change again), so likely won't be able to happen anymore in 8.x cycle.

Fabianx’s picture

Status: Needs review » Needs work

Looks fantastic, so much more clear!

Here is a review:

  1. +++ b/core/modules/block/src/BlockRepository.php
    @@ -52,13 +59,15 @@ public function __construct(EntityManagerInterface $entity_manager, ThemeManager
         $active_theme = $this->themeManager->getActiveTheme();
    +    $blocks = $this->getBlocksForTheme();
    
    @@ -84,4 +93,15 @@ public function getVisibleBlocksPerRegion(array $contexts, array &$cacheable_met
    +  public function getBlocksForTheme() {
    +    $active_theme = $this->themeManager->getActiveTheme();
    

    I agree with Tim, it is better to pass the active_theme to this function.

  2. +++ b/core/modules/block/src/ContextProvider/CurrentLanguageContext.php
    @@ -54,16 +65,18 @@ public function onBlockActiveContext(BlockContextEvent $event) {
    -        $event->setContext('language.' . $type_key, $context);
    +        $result['language.' . $type_key] = $context;
    

    Yahoo!

  3. +++ b/core/modules/block/src/ContextProvider/CurrentLanguageContext.php
    @@ -54,16 +65,18 @@ public function onBlockActiveContext(BlockContextEvent $event) {
    +  public function getConfigurationTimeContexts() {
    +    return $this->getRunTimeContexts([]);
       }
    

    I would feel better to be able to omit the argument completely.

  4. +++ b/core/modules/block/src/ContextProvider/CurrentUserContext.php
    @@ -60,14 +61,18 @@ public function onBlockActiveContext(BlockContextEvent $event) {
    -    $event->setContext('user.current_user', $context);
    ...
    +    $result = [];
    +    $result['user.current_user'] = $context;
    +
    +    return $result;
    

    So much better to have a getter return something!

  5. +++ b/core/modules/block/src/ContextProvider/CurrentUserContext.php
    @@ -60,14 +61,18 @@ public function onBlockActiveContext(BlockContextEvent $event) {
    +  public function getConfigurationTimeContexts() {
    +    return $this->getRunTimeContexts([]);
       }
    

    Same as above.

  6. +++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
    @@ -129,10 +134,10 @@ public function build() {
    -    $cacheable_metadata_list = [];
    ...
    +    $cacheable_metadata = [];
    
    @@ -180,7 +185,7 @@ public function build() {
    -    foreach ($cacheable_metadata_list as $cacheable_metadata) {
    +    foreach ($cacheable_metadata as $cacheable_metadata) {
    

    Uhm, that feels like FAIL to me ...

    CNW

  7. +++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
    @@ -129,10 +134,10 @@ public function build() {
    +    $theme_blocks = $this->blockRepository->getBlocksForTheme();
    

    Ah, this is where we don't have the active theme, then maybe rename to getBlocksForActiveTheme() might be indeed better.

dawehner’s picture

Status: Needs work » Needs review
FileSize
56.26 KB
15.17 KB

Here is an intermediate state. Posting for berdir for review

@fabian
This is maybe not addressing all your points yet

Status: Needs review » Needs work

The last submitted patch, 186: 2354889-184.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
FileSize
57.75 KB
6.25 KB

Uhm, that feels like FAIL to me ...

CNW

Please clarify why this feels like FAIL for you.

Status: Needs review » Needs work

The last submitted patch, 188: 2354889-187.patch, failed testing.

Fabianx’s picture


$cacheable_metadata = ['a', 'b'];

foreach ($cacheable_metadata as $cacheable_metadata) {
  print $cacheable_metadata . "\n";
}

print_r($cacheable_metadata);

I am curious what happens on 3v4l: http://3v4l.org/TvfSb

Yes, it works, but it feels wrong to overwrite the variable used with the variable used in the inner loop as $cacheable_metadata in the end is 'b', so that could lead easily to side-effects.

catch’s picture

Status: Needs work » Needs review

Overall this looks great. Few things I noticed between nits and minor.

  1. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextManager.php
    @@ -0,0 +1,109 @@
    +   */
    +  public function getRunTimeContexts(array $context_ids) {
    +    $remaining_context_ids = array_diff($context_ids, array_keys($this->contexts));
    +    $already_existing_context_ids = array_diff(array_keys($this->contexts), $context_ids);
    +    $contexts = array_intersect_key($this->contexts, array_flip($already_existing_context_ids));
    +
    +    // Create a map of context providers (service IDs) to context slot names.
    +    $context_slot_names_by_service = [];
    +    foreach ($remaining_context_ids as $id) {
    

    I wonder a bit why we don't just check if (isset($this->contexts[$id])) in the foreach once or twice rather than the two array_diff() and array_intersect_key() up top. I kept checking where the variables are used below and the answer is not much.

  2. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextManagerInterface.php
    @@ -0,0 +1,35 @@
    +  /**
    +   * Gets all context values for the purposes of configuration.
    +   *
    +   * @return \Drupal\Core\Plugin\Context\ContextInterface[]
    +   *   All available contexts.
    +   */
    +  public function getConfigurationTimeContexts();
    +
    +}
    

    From reading this I have a bit of trouble figuring out what a 'configuration time context' is. We don't care what the context values are at configuration time, but it returns context interface instances - also see below.

  3. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextProviderInterface.php
    @@ -0,0 +1,66 @@
    +
    

    This interface looks exactly the same as the one above, except with 10 times the docs. If we need two interfaces, at least the manager could extend from the provider?

  4. +++ b/core/modules/block/block.services.yml
    @@ -7,21 +7,6 @@ services:
           - { 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:
    

    Nice.

  5. +++ b/core/modules/block/src/BlockForm.php
    @@ -117,7 +122,7 @@ public function form(array $form, FormStateInterface $form_state) {
         // during form building.
    -    $form_state->setTemporaryValue('gathered_contexts', $this->dispatcher->dispatch(BlockEvents::ADMINISTRATIVE_CONTEXT, new BlockContextEvent())->getContexts());
    +    $form_state->setTemporaryValue('gathered_contexts', $this->contextManager->getConfigurationTimeContexts());
     
         $form['#tree'] = TRUE;
    

    Nice.

  6. +++ b/core/modules/block/src/BlockRepositoryInterface.php
    @@ -7,6 +7,11 @@
    + *
    + * It allows you to filter blocks by region as well as get all blocks by theme.
    + */
    

    Nit: probably 'Allows filtering of blocks by region and getting all blocks for a theme'.

  7. +++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
    @@ -180,7 +197,7 @@ public function build() {
         $merged_cacheable_metadata = CacheableMetadata::createFromRenderArray($build);
    -    foreach ($cacheable_metadata_list as $cacheable_metadata) {
    +    foreach ($cacheable_metadata as $cacheable_metadata) {
           $merged_cacheable_metadata = $merged_cacheable_metadata->merge($cacheable_metadata);
         }
    

    As above even if this works by accident let's not do it.

  8. +++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
    @@ -189,13 +206,35 @@ public function build() {
    +  protected function getRunTimeContexts(array $theme_blocks) {
    +    $context_provider_ids = [];
    +    $contexts = [];
    +    foreach ($theme_blocks as $block_id => $block) {
    +      /* @var \Drupal\block\BlockInterface $block */
    +      foreach ($block->getVisibilityConditions() as $condition) {
    +        if ($condition instanceof ContextAwarePluginInterface) {
    +          /* @var \Drupal\Core\Plugin\ContextAwarePluginInterface $condition */
    +          $context_provider_ids = array_unique(array_merge($context_provider_ids, $condition->getContextMapping()));
    +        }
    +      }
    +    }
    +    if ($context_provider_ids) {
    +      $contexts = $this->contextManager->getRunTimeContexts($context_provider_ids);
    +    }
    +    return $contexts;
       }
     
    

    Looks great.

catch’s picture

Status: Needs review » Needs work

Sorry cross-posted on the status.

Berdir’s picture

  1. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextManager.php
    @@ -0,0 +1,109 @@
    + * Manages context results.
    + *
    + * Provides methods to fetch all available contexts at configuration-time or a
    + * subset at run-time.
    + */
    +class ContextManager implements ContextManagerInterface {
    

    Not sure about the documentation here, part of this should be on the interface if it's not really there. It's just the implementation of that interface, not really anything relevant to add, what do we usually do for that?

  2. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextManager.php
    @@ -0,0 +1,109 @@
    +      // The IDs have been passed in {context_slot_name}@{service_id} format.
    +      if (strpos($id, '@') !== FALSE) {
    +        list($context_slot_name, $id) = explode('@', $id, 2);
    +      }
    

    I discussed the naming a bit with @alexpott and @dawehner. Neither of us likes slot_name, that's just not what it is.

    Given that this only exists between the manager and the providers, we think that we can go with a slightly longer and explicit name, possibly $unqualified_context_id?

    Additionally, since this is a hierarchy, we think that it would make more sense to structure them like this: "service_id:unqualified_context_id".

    And the last point, I think we should remove the manual namespacing that was introduced a while back, since now the unqualified context id just needs to be unique for a given service. So NodeRouteContext can use just "node" again, not "node.node". If we change it anyway, then it doesn't really matter if we rename the known unqualified names as well?

  3. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextManager.php
    @@ -0,0 +1,109 @@
    +      $context_slot_names_by_service[$id][] = $context_slot_name;
    

    this could maybe just be $context_ids_by_service? not sure if we need to repeat unqualified, could get quite long.

  4. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextManager.php
    @@ -0,0 +1,109 @@
    +    // Iterate over all context providers (services), gather the run-time
    +    // contexts and assign them to the slots as requested.
    

    we don't really iterate over *all* services

  5. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextManager.php
    @@ -0,0 +1,109 @@
    +      $context_results = $this->container->get($service_id)->getRunTimeContexts($context_slot_names);
    

    $contexts_by_service?

  6. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextManager.php
    @@ -0,0 +1,109 @@
    +      $wanted_contexts = array_intersect_key($context_results, array_flip($context_slot_names));
    +      foreach ($wanted_contexts as $context_slot_name => $context) {
    +        $context_id = $context_slot_name . '@' . $service_id;
    +        $contexts[$context_id] = $context;
    +      }
    

    I think this would be a easier to read if you just loop over $unqualified_context_ids and then get it from $contexts_by_service[$...] (including an isset check)

    We should probably also document explicitly on the interface that it's not guaranteed that a context can be found. context mapper then later throws an exception if that happens. but throwing one here could be tricky.

  7. +++ b/core/modules/block/src/BlockRepository.php
    @@ -32,6 +32,13 @@ class BlockRepository implements BlockRepositoryInterface {
    +  /**
    
    @@ -52,13 +59,14 @@ public function __construct(EntityManagerInterface $entity_manager, ThemeManager
           /** @var \Drupal\block\BlockInterface $block */
    -      $block->setContexts($contexts);
    +      $block->setContexts($contexts + $block->getContexts());
           $access = $block->access('view', NULL, TRUE);
    

    Yes, definitely confused by this line now.

  8. +++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
    @@ -189,13 +206,35 @@ public function build() {
    +   * Whilst these blocks may be placed, we cannot determine whether they are
    +   * visible until after context values have been calculated and provided to
    +   * the block so that access and therefore visibility can be evaluated.
    

    i'm not exactly sure what this documentation is telling me and why it's on *this* method?

    Maybe this should be part of the class docblocks as part of a bigger summary that explains what the class is doing?

  9. +++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
    @@ -189,13 +206,35 @@ public function build() {
    +  protected function getRunTimeContexts(array $theme_blocks) {
    

    I'm wondering if all this logic should not completely move to the BlockAccessControlHandler. It would possibly be slightly slower but context manager now deals with already loaded things.

    The advantage is that we could remove $contexts completely from being passed around between this and block repository and then through setContexts() and getContexts() of the block entity to the block access control handler. We'd just load the contexts of the block we need in the access control handler and use it there. thoughts?

  10. +++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
    @@ -189,13 +206,35 @@ public function build() {
    +        if ($condition instanceof ContextAwarePluginInterface) {
    +          /* @var \Drupal\Core\Plugin\ContextAwarePluginInterface $condition */
    +          $context_provider_ids = array_unique(array_merge($context_provider_ids, $condition->getContextMapping()));
    +        }
    

    According to the implementation in ContextHandler, context mapping can also happen automatically if the context_id's match. We possibly need to care about that too, but I'm actually not sure anymore what that means with the fully qualified names. Maybe that logic should simply be removed?

  11. +++ b/core/modules/block/src/Tests/BlockLanguageTest.php
    @@ -90,7 +90,7 @@ public function testLanguageBlockVisibilityLanguageDelete() {
               ),
    -          'context_mapping' => ['language' => 'language.language_interface'],
    +          'context_mapping' => ['language' => 'language.language_interface@block.current_language_context'],
    

    The service name hasn't been updated yet, with all the suggestions above, I guess this could become:

    language.current_language_context:language_interface.

dawehner’s picture

Status: Needs work » Needs review
FileSize
58.94 KB
8.62 KB

Oh yes , I totally agree its wrong how it is, so let's rename it to cacheable_metadata_list, ... given that its an array of it

I wonder a bit why we don't just check if (isset($this->contexts[$id])) in the foreach once or twice rather than the two array_diff() and array_intersect_key() up top. I kept checking where the variables are used below and the answer is not much.

Oh Fuck, so much better!

This interface looks exactly the same as the one above, except with 10 times the docs. If we need two interfaces, at least the manager could extend from the provider?

Yeah we talked about that ... well the problem is that the keys are different. Once its with the @ and once without.

From reading this I have a bit of trouble figuring out what a 'configuration time context' is. We don't care what the context values are at configuration time, but it returns context interface instances - also see below.

Yeah that is the fun of context objects, they might or might not have data.

Nit: probably 'Allows filtering of blocks by region and getting all blocks for a theme'.

Sure, even I worked for a while to get it below 80 chars before.

dawehner’s picture

FileSize
59.02 KB
10.83 KB

Adressing some of the feedback from @berdir

Berdir’s picture

1
This interdiff is basically what I'm suggesting above. Not posting full patch yet, the unit tests would explode but it works fine locally when I tested it manually with a

The optimization from getting all context ids first is gone, but that should only be a very slight regression since ContextManager now has the static cache built in.

By injecting the manager into BlockAccessControlHandler and fetching just the contexts we need there, we can remove the get/setContext methods from BlockInterface, can remove all context things from BlockPageDisplay, can remove $contexts from BlockRepository *and* make getBlocksForTheme() protected.

2
We now have ContextManager and ContextHandler. It seems very hard to explain what the manager is managing and the handler is handling and what the difference between the two.

What if.... we just merged the two new methods into ContextHandler? Or if not, make the new ContextManager name more specific.. maybe something like ContextDiscovery? Or another fancy word for "looking up contexts". Repository?

3
Last poing. We are kind of defining this new thing as a generic service that others can use as well. There's at least one problem there. If you compare it with Page Manager contexts, there's one major difference. Many contexts for Page Manager are specific to the page and need the page (e.g. configuration from it, or the route, ...). I'm sure that other cases will have similar things. I don't know if we want to try and solve this in core, but if we do, then we need.... context for the context providers? An array with arbitrary stuff ($options ?) that other use cases could pass along what they want and then providers can use it?

catch’s picture

Yeah we talked about that ... well the problem is that the keys are different. Once its with the @ and once without.

That's unfortunate. This could use an explanation and an @see then?

dawehner’s picture

That's unfortunate. This could use an explanation and an @see then?

Yes we could extend the interface maybe?

catch’s picture

So we'd extend the interface, but the structure of the return value would be slightly different. I think if that's documented it's probably OK but seems borderline.

EclipseGc’s picture

  1. +++ b/core/modules/block/src/BlockRepository.php
    @@ -50,22 +57,23 @@ public function __construct(EntityManagerInterface $entity_manager, ThemeManager
    +      $block->setContexts($contexts + $block->getContexts());
    

    Oh wow wtf...

  2. +++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
    @@ -189,13 +206,35 @@ public function build() {
    +  protected function getRunTimeContexts(array $theme_blocks) {
    +    $context_provider_ids = [];
    +    $contexts = [];
    +    foreach ($theme_blocks as $block_id => $block) {
    +      /* @var \Drupal\block\BlockInterface $block */
    +      foreach ($block->getVisibilityConditions() as $condition) {
    +        if ($condition instanceof ContextAwarePluginInterface) {
    +          /* @var \Drupal\Core\Plugin\ContextAwarePluginInterface $condition */
    +          $context_provider_ids = array_unique(array_merge($context_provider_ids, $condition->getContextMapping()));
    +        }
    +      }
    +    }
    +    if ($context_provider_ids) {
    +      $contexts = $this->contextManager->getRunTimeContexts($context_provider_ids);
    +    }
    +    return $contexts;
    

    Why don't we check the block's own context mapping in this function as well?

  3. +++ b/core/modules/comment/comment.module
    @@ -339,7 +339,7 @@ function comment_entity_storage_load($entities, $entity_type) {
    +  if (!\Drupal::hasService('comment.manager') || !\Drupal::service('comment.manager')->getFields($entity_type)) {
    

    Is this scope creep?

  4. +++ b/core/modules/language/src/ContextProvider/CurrentLanguageContext.php
    @@ -41,10 +41,20 @@ public function __construct(LanguageManagerInterface $language_manager) {
    +        if (array_search(str_replace('language.', '', $context_slot_name), $language_types) === FALSE) {
    +          unset($language_types[str_replace('language.', '', $context_slot_name)]);
    +        }
    

    If $key does not appear as a value in $array, unset $array[$key]???

    Am I reading that wrong? Sounds a lot like array_filter() to me if we're expecting identical key/value pairings with a 0 value under certain conditions.

Looking at berdir's interdiff, a lot of the code I commented on is removed. The primary arch change in this issue appears to be instead of "getting active contexts" we "get mapped contexts" and then individually invoke each of the services responsible for a context we know we need, so this is lazy context-instantiation. Ideally that sounds good, and for core it is. So long as this is good for core and it doesn't block contrib, I'll be ++.

196.2:
I'd prefer something like LazyContextRepository or LazyContextFactory. Technically, this is an awful lot like a sub-service-container. We're defining services it cares about, and then statically caching objects for use lazily. Simply "ContextRepository" is probably fine. This should definitely NOT be merged with the context handler class in any way.

196.3:
I think that ship has sailed. Solving this for contrib, ideally would be awesome, but it's too big of a problem space at this point in the game. 8.1.x?

Eclipse

Fabianx’s picture

#196 looks great to me.

I think adding an optional option for enabling contrib to use it would be okay.

And I am in strong favor of keeping it generic.

dawehner’s picture

Is this scope creep?

No, this has been needed in order to fix some tests, see #2354889-74: Make block context faster by removing onBlock event and replace it with loading from a ContextManager

dsnopek’s picture

+1 to moving the ContextProviders out of the 'block' module, which was done in the patch on #186! This is what I was asking for in #132 and it looks beautiful. :-) Thanks!

#196.3: Looking at D7 CTools, it appears that context plugins don't need to take any context, but relationship plugins (which are kind of a special context provider) do. So, it seems likely that contrib would probably need the ability for context providers to take both context and some amount of config. :-/ I guess, if necessary, we'll need to add this ability on in CTools for D8, since like EclipseGC said above, it's probably too late to squeak that in.

Doing some experimental patches to adapt Page Manager to work with the service from this patch would help get some perspective on this. If I have time tomorrow, I may give that a shot.

larowlan’s picture

Working on this again

catch’s picture

larowlan’s picture

The optimization from getting all context ids first is gone, but that should only be a very slight regression since ContextManager now has the static cache built in.

But I'm not sure we're using it as we always call the ->getRunTimeContexts method - addressing that and tests as well as renaming to LazyContextRepository which is my preferred name

larowlan’s picture

The optimization from getting all context ids first is gone, but that should only be a very slight regression since ContextManager now has the static cache built in.

Ignore me, we only fetch those which we don't have already

larowlan’s picture

Issue summary: View changes
Issue tags: +needs profiling
FileSize
28.27 KB
60.18 KB

Renamed to LazyContextRepository(Interface)
Used substr instead of str_replace for micro-optimisation
Fixed exception message and test
Fixed unit tests
Updated IS with up to date API changes, which are shrinking with each pass
Added back the needs profiling tag as we could use a fresh round in light of new changes

dawehner’s picture

Issue summary: View changes
Issue tags: -needs profiling

Certainly +1 for using repository as part of the name.

Same test scenario, the numbers are even better this time.

larowlan’s picture

https://www.drupal.org/node/2527840 for draft change notice
More naming changes as discussed on irc with berdir|dawehner

Berdir’s picture

I've discussed #196 with EclipseGc.

1
His biggest concern was that the block page variant still has full control over what's happen with blocks, contexts and so on.

While my patch removes a bit of control from it, both the BlockRepository and BlockAccessControlHandler classes are still an implementation detail and block.module specific, a page_manager replacement for example simply not call them.

Given that, he is OK with moving the context stuff to block access control handler directly.

He was also very surprised to learn that block plugins do not get context assigned *ever* in HEAD. It looks like that is definitely supposed to happen, that's why BlockRepository injects ContextHandler but it is never used. We should leave it, and then his block context UI patch can then just also inject the block discovery service and get the contexts and assign them there.

2. EclipseGc was strongly against merging the handler and the manager, but he agreed manager isn't the best name. As written above already, we agreed on LazyBlockRepository and in IRC with @dawehner and @larowlan we just decided to only use that on the implementation and use BlockRepositoryInterface and context.repository as the service name.

3. Agreed with what's been said. We can keep the service out there, if someone wants to use it, great, if not, then page_manager can for example extend it and provide additional methods that are specific for it. Adding $options seems a bit weird when we have no use cases for it in core. And we have no idea if it would really work out for contrib...

larowlan’s picture

Issue summary: View changes

Status: Needs review » Needs work

The last submitted patch, 210: block-context-2354889-2.210.patch, failed testing.

larowlan’s picture

Status: Needs work » Needs review
FileSize
594 bytes
60.04 KB
larowlan’s picture

Assigned: larowlan » Unassigned
Wim Leers’s picture

Another review of mine that's likely to be annoying. Let me know if you want me to stop reviewing this. I just hope that my perspective is useful — of somebody who doesn't know the context system at all, except at a conceptual level. Contexts as a concept make total sense to me, I just have a lot of trouble understanding the naming/implementation I guess…

  1. +++ b/core/core.services.yml
    @@ -1403,7 +1406,6 @@ services:
         class: Egulias\EmailValidator\EmailValidator
    -
       response_filter.active_link:
    

    Nit: unnecessary change.

  2. +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/ContextProvidersPass.php
    @@ -0,0 +1,34 @@
    +   * Adds the IDs of services tagged as context providers service IDs to the
    

    Too many service IDs :)

  3. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextProviderInterface.php
    @@ -0,0 +1,66 @@
    + * Defines an interface for providing plugin contexts.
    

    Eh "providing plugin contexts" — shouldn't that be "providing plugins with contexts"?

    Perhaps also s/an interface/a service interface/? Not sure about that at all though.

  4. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextRepositoryInterface.php
    @@ -0,0 +1,38 @@
    +/**
    + * Manages available contexts and collects context values at run-time.
    + *
    + * Provides methods to fetch all available contexts at configuration-time or a
    + * subset at run-time.
    + */
    +interface ContextRepositoryInterface {
    +
    +  /**
    +   * Gets run-time context values for the given context IDs.
    +   *
    +   * @param string[] $context_ids
    +   *   Context provider IDs to get contexts for. These must be in the
    +   *   {context_slot_name}@{service_id} format.
    +   *
    +   * @return \Drupal\Core\Plugin\Context\ContextInterface[]
    +   *   The determined contexts.
    +   */
    +  public function getRunTimeContexts(array $context_ids);
    +
    +  /**
    +   * Gets all context values for the purposes of configuration.
    +   *
    +   * @return \Drupal\Core\Plugin\Context\ContextInterface[]
    +   *   All available contexts.
    +   */
    +  public function getConfigurationTimeContexts();
    +
    +}
    

    This is still quite confusing IMHO.

    The first line in the class docblock suggests it only cares about run-time contexts. But then the remainder says it's about all contexts.

    The fact that there's a clear distinction between "run time contexts" and "configuration time contexts" sounds sensible. This is how I understood the patch so far. I.e.: my understanding:

    • configuration time contexts have the entire context pre-configured (e.g. stored in the placed block config entity == configuration time): for example, the Feed entity that a block should show the Feed Items for
    • run time contexts only have a context plugin assigned, and therefore a context provider service is invoked during the request lifetime (== at run time): for example, a "page view statistics" block that needs a node context is configured to use the NodeRouteContet

    Yet given that understanding/world view, the documentation on getConfigurationTimeContexts() makes no sense at all: "for the purposes of configuration" and "all available contexts" made me go "WTF?". Reading the class docblock brings clarity: apparently "configuration time contexts" actually means "all contexts in the entire system"?

    This suggests I'm completely misunderstanding the system.

    If a method returns all contexts in the system, why not call it getAllContexts() or something similarly non-ambiguous?

  5. +++ b/core/lib/Drupal/Core/Plugin/Context/LazyContextRepository.php
    @@ -0,0 +1,111 @@
    + * Manages context results.
    ...
    +class LazyContextRepository implements ContextRepositoryInterface {
    

    "result" vs. "lazy".

    If something manages *results*, then evaluation has already happened, so where's the lazy evaluation then?

    Confusing.

  6. +++ b/core/lib/Drupal/Core/Plugin/Context/LazyContextRepository.php
    @@ -0,0 +1,111 @@
    + * Provides methods to fetch all available contexts at configuration-time or a
    + * subset at run-time.
    

    Adds to my confusion from above.

EclipseGc’s picture

@Wim

So a clarification. Runtime contexts vs Configuration has a few nuances.

  1. ContextInterface objects are a wrapper around a DataDefinition and some data
  2. Runtime contains a real data object AND a DataDefinition
  3. Configuration CAN contain a data object but MUST provide a DataDefinition
  4. Configuration time MUST include all contexts a context provide CAN provide
  5. Runtime MUST provide only configured plugin contexts (this is specific to core, contrib need not use this process)

This last point is why I suggested we call it "Lazy". It is perhaps not lazy in the sense that PluginCollections are lazy, but runtime contexts are only instantiated if necessary in this implementation.

Eclipse

dawehner’s picture

FileSize
59.76 KB
3.06 KB

Too many service IDs :)

I think I found something better.

providing plugins with contexts

Ehm, no, context are not plugins. They provide those value objects, which then are used by plugins.

So yeah contexts are basically a maybe monad with an additional data definition.

Adds to my confusion from above.

Yeah, let's move that onto the interface.

Fabianx’s picture

To explain #217 with an example:

It is actually really simple, because it works conceptually exactly like tokens in D7 (contrib):

  • - You have a token tree that shows all tokens available. (configuration / admin)
  • - However you are able to give information to the token tree builder that e.g. this 'object type' is available and needs tokens, too. (e.g. entity_type => 'node') [ I would have to lookup the exact API. ]
  • - When you select a token [node:title] and you use it, then for the replacement purposes you have to add a concrete $node object in, whose tokens can be replaced.

There is hook_tokens() and hook_token_info().

  • - hook_token_info() is the data definition.
  • - hook_tokens() is the actual implementation.

Compare that to:

  1. ContextInterface objects are a wrapper around a DataDefinition and some data
  2. e.g.

    $context = new Context(new ContextDefinition('entity:node', NULL, FALSE));
    $contexts['node.node'] = $context;
    
  3. Runtime contains a real data object AND a DataDefinition
  4. $context = new Context(new ContextDefinition('entity:node'));
    $context->setContextValue($node);
    $contexts['node.node'] = $context;
    
  5. Configuration CAN contain a data object but MUST provide a DataDefinition
  6. e.g. both 1 and 2 would work.

  7. Configuration time MUST include all contexts a context provide CAN provide
  8. $context1 = new Context(new ContextDefinition('entity:node', NULL, FALSE));
    $contexts['node.node'] = $context1;
    
    $context2 = new Context(new ContextDefinition('foo:bar', NULL, FALSE));
    $contexts['foo.bar'] = $context2;
    
  9. Runtime MUST provide only configured plugin contexts (this is specific to core, contrib need not use this process)
  10. In general this means that I can ask specifically for foo.bar, but node.node must not be computed.

    if (in_array($wanted_contexts, 'foo.bar')) {
      $context2 = new Context(new ContextDefinition('foo:bar', NULL, FALSE));
      $contexts['foo.bar'] = $context2;
    }
    

That is how far I have understood it.

dawehner’s picture

Berdir’s picture

Assigned: Unassigned » Berdir

Working on this, making some renaming and other changes that we discussed.

Berdir’s picture

FileSize
50.49 KB
15.11 KB

A few changes, might need test updates.

* Renamed getConfigurationTimeContexts() to getAvailableContexts(). We discussed it here and I think the previous name was based on a misunderstanding by @Wim Leers. Funny enough, the docblock already used "available"

* Since RunTime now longer needs to be consistent with ConfigurationTime, I renamed that to Runtime.

* I removed the manual namespacing of the unqualified context ID's in the providers, so node.node is now just node., user.current_user is just current_user. You can see in the updated language related test that we still have language in there *three* times. That's plenty :p

* I also removed all notions of slot_name that were added and used qualified/unqualified context ID as discussed before. There's still *one* place that mentions of context_slot in core, but that's contained to local variables in a trait.

Status: Needs review » Needs work

The last submitted patch, 222: 2512866-111.patch, failed testing.

Berdir’s picture

Status: Needs work » Needs review
FileSize
60.04 KB
23.69 KB

Wrong patch.

EclipseGc’s picture

Status: Needs review » Needs work
  1. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextProviderInterface.php
    @@ -25,16 +32,17 @@
    +   *   return ['node' => $context];
    

    I don't know if we want to document this this way. The ContextHandler does a magic-name mapping if no mapping is provided, so a plugin defining a context key of 'node' would auto-map to a context in the context repository with the same key.

    I've not ever been thrilled with that functionality, and it complicates the docs here.

  2. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextProviderInterface.php
    @@ -47,20 +55,20 @@ public function getRunTimeContexts(array $context_slot_names);
    +  public function getAvailableContexts();
    

    Sorry, I guess I didn't explicitly disagree with Wim's comment on this. I feel like the previous method name was much clearer in terms of what it does. It gets context definitions available during configuration. That's what it does.

  3. +++ b/core/modules/node/src/ContextProvider/NodeRouteContext.php
    @@ -62,9 +62,9 @@ public function getRunTimeContexts(array $context_slot_names) {
    +    return ['node' => $context];
    

    Same thing as before. We chose not to do this to prove that real mapping was actually functioning. I'm not committed to one direction or the other, but the magic mapping complicates decisions around this imo. I, for one, would prefer a key like 'route.node' or something.

  4. +++ b/core/modules/user/src/ContextProvider/CurrentUserContext.php
    @@ -62,7 +62,7 @@ public function getRunTimeContexts(array $context_slot_names) {
    +      'current_user' => $context,
    

    Continuing to look at the key changes here, we were prefixing these by module we ideally wanted them to exist in. It's funny that now that they're in the appropriate module we lost that prefixing. I have fewer issues with this one but thought it at least worth pointing out.

dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextProviderInterface.php
    @@ -9,6 +9,12 @@
    + * The fully qualified context ID then includes the service ID:
    + * @service_id:unqualified_context_id.
    

    +1 for that comment

  2. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextProviderInterface.php
    @@ -25,16 +32,17 @@
    -   *   return ['node.node' => $context];
    +   *   return ['node' => $context];
    

    That is a good improvement IMHO!

  1. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextProviderInterface.php
    @@ -0,0 +1,74 @@
    +   * @param array $unqualified_context_ids
    

    Let's use string[]

  2. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextRepositoryInterface.php
    @@ -0,0 +1,40 @@
    + * for configuration on forms, as well as a method to determine the congrete
    

    concrete

  3. +++ b/core/lib/Drupal/Core/Plugin/Context/LazyContextRepository.php
    @@ -0,0 +1,108 @@
    +    // Create a map of context providers (service IDs) to unqualified context IDs.
    

    80 chars

  4. +++ b/core/lib/Drupal/Core/Plugin/Context/LazyContextRepository.php
    @@ -0,0 +1,108 @@
    +    // run-time contexts and assign them as requested.
    

    Let's use runtime now

  5. +++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
    @@ -7,19 +7,18 @@
    +use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
    ...
    +use Drupal\Core\Plugin\ContextAwarePluginInterface;
    

    Not longer needed

  6. +++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
    @@ -7,19 +7,18 @@
    +use Drupal\Core\Theme\ThemeManagerInterface;
    
    @@ -61,6 +60,13 @@ class BlockPageVariant extends VariantBase implements PageVariantInterface, Cont
     
       /**
    +   * The theme manager.
    +   *
    +   * @var \Drupal\Core\Theme\ThemeManagerInterface
    +   */
    +  protected $themeManager;
    +
    +  /**
        * The render array representing the main page content.
    
    @@ -80,17 +86,17 @@ class BlockPageVariant extends VariantBase implements PageVariantInterface, Cont
    -   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
    -   *   The event dispatcher.
    +   * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
    +   *   The theme manager.
        * @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, ThemeManagerInterface $theme_manager, 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;
    +    $this->themeManager = $theme_manager;
       }
     
       /**
    @@ -103,7 +109,7 @@ public static function create(ContainerInterface $container, array $configuratio
    
    @@ -103,7 +109,7 @@ public static function create(ContainerInterface $container, array $configuratio
           $plugin_definition,
           $container->get('block.repository'),
           $container->get('entity.manager')->getViewBuilder('block'),
    -      $container->get('event_dispatcher'),
    +      $container->get('theme.manager'),
           $container->get('entity.manager')->getDefinition('block')->getListCacheTags()
         );
       }
    @@ -129,10 +135,9 @@ public function build() {
    

    Not longer needed

  7. +++ b/core/modules/block/tests/src/Unit/BlockFormTest.php
    @@ -104,7 +104,7 @@ public function testGetUniqueMachineName() {
    diff --git a/core/modules/block/tests/src/Unit/BlockRepositoryTest.php b/core/modules/block/tests/src/Unit/BlockRepositoryTest.php
    
    diff --git a/core/modules/block/tests/src/Unit/BlockRepositoryTest.php b/core/modules/block/tests/src/Unit/BlockRepositoryTest.php
    index d77d64e..e3f2e97 100644
    
    index d77d64e..e3f2e97 100644
    --- a/core/modules/block/tests/src/Unit/BlockRepositoryTest.php
    
    --- a/core/modules/block/tests/src/Unit/BlockRepositoryTest.php
    +++ b/core/modules/block/tests/src/Unit/BlockRepositoryTest.php
    

    There is some empty $contexts = [] in here which is no longer needed

  8. +++ b/core/modules/block/tests/src/Unit/Plugin/DisplayVariant/BlockPageVariantTest.php
    @@ -46,6 +40,13 @@ class BlockPageVariantTest extends UnitTestCase {
       /**
    +   * The mocked theme manager.
    +   *
    +   * @var \Drupal\Core\Theme\ThemeManagerInterface||\PHPUnit_Framework_MockObject_MockObject
    +   */
    +  protected $themeManager;
    +
    
    @@ -71,12 +72,14 @@ public function setUpDisplayVariant($configuration = array(), $definition = arra
    +
    +    $this->themeManager = $this->getMock('Drupal\Core\Theme\ThemeManagerInterface');
    +    $this->themeManager->expects($this->any())
    +      ->method('getActiveTheme')
    +      ->willReturn((new ActiveTheme(['name' => 'active_theme'])));
    +
         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, $this->themeManager, ['config:block_list']))
           ->setMethods(array('getRegionNames'))
           ->getMock();
    

    Yeah not needed either

Berdir’s picture

Status: Needs work » Needs review
FileSize
58.96 KB
10.9 KB

Wow, #228 already.

This should address that review, thanks!

I'll work on the issue summary next.

Fabianx’s picture

#225:

The reason, why neither 'route.node' nor 'node.node' makes sense with the approach here anymore is that in the end the UI / code deals with:

  • - '@user.current_user_context:current_user'
  • - '@node.node_route_context:node'

so either

  • - '@user.current_user_context:user.current_user'
  • - '@node.node_route_context:node.node'

or

  • - '@user.current_user_context:current_user.user'
  • - '@node.node_route_context:route.node'

would have a lot of redundancy.


[ Edit: Deleted very wrong comment about the wrong service IDs that I made because of dealing with monkeys that broke out of the control room and wrecked havoc with my ErrorContainer ...]


Overall patch looks good.

Berdir’s picture

Discussed the review in #225 with EclipseGc.

1/3/4 is fine, because the actual context_id is '@node.node_route_context:node'. Nobody except the provider will see the short value. I've also discussed with EclipseGc that we should probably remove that default-mapping logic. It's not going to do anything ever unless you name your required context on your plugin exactly like that. And nobody will do that. But not in this issue.

2. Discussed on IRC. He can live with the name. Let's get some more feedback on that, however.

EclipseGc’s picture

Status: Needs review » Reviewed & tested by the community

I think we're all good to go on this issue.

Eclipse

tim.plunkett’s picture

Just to pile on, I am also happy with this change.
RTBC +1

Fabianx’s picture

Issue summary: View changes

RTBC + 1 - Looks really great now!


Proposed commit message:

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

Updating the commit credits to (hopefully) match #233.

Not committing yet because I need to catch up with #222 onwards, this should not stop someone else doing so if they get here first.

Wim Leers’s picture

Anothering Annoying-Wim-Is-Asking-Annoying-Questions-Review ®.

This patch is significantly more understandable already than in prior iterations! :)

  1. +++ b/core/core.services.yml
    @@ -266,6 +266,9 @@ services:
    +  context.repository:
    +    class: Drupal\Core\Plugin\Context\LazyContextRepository
    

    The service name omits the "lazy" adjective. I'm sure this is intentional. But does this mean that it'll ever make sense to have a non-lazy repository then?

    Because if laziness is always desired, then that's an adjective that doesn't really add meaning here.

    e.g. "LazyContainer" doesn't make sense because the whole point of a container is that it's lazy. If the same applies here, let's omit that adjective.

  2. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextProviderInterface.php
    @@ -0,0 +1,76 @@
    +interface ContextProviderInterface {
    
    +++ b/core/lib/Drupal/Core/Plugin/Context/ContextRepositoryInterface.php
    @@ -0,0 +1,40 @@
    +interface ContextRepositoryInterface {
    

    Perfect symmetry between these two interfaces' method names! Yay!

    This greatly enhances understandability. It makes it immediately clear that the repository interface just returns the joined set of return values by all providers.

    Perhaps documenting this on the repository interface would be a good thing, to make this also clear while navigating the code, rather than only when looking at this patch?

  3. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextProviderInterface.php
    @@ -0,0 +1,76 @@
    +   * Provides the runtime contexts.
    
    +++ b/core/lib/Drupal/Core/Plugin/Context/ContextRepositoryInterface.php
    @@ -0,0 +1,40 @@
    +   * Gets runtime context values for the given context IDs.
    

    Inconsistent.

  4. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextProviderInterface.php
    @@ -0,0 +1,76 @@
    +   * @param string[] $unqualified_context_ids
    +   *   The requested context IDs. The context provider must only return contexts
    +   *   for those IDs.
    
    +++ b/core/lib/Drupal/Core/Plugin/Context/ContextRepositoryInterface.php
    @@ -0,0 +1,40 @@
    +   * @param string[] $context_ids
    +   *   Fully qualified context IDs. These must be in the
    +   *   @service_id:unqualified_context_id format, for example
    +   *   node.node_route_context:node.
    

    These are mostly consistent, but it'd be great to document what it takes to be a "fully qualified" context ID, and therefore also what an "unqualified" context ID is.

  5. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextProviderInterface.php
    @@ -0,0 +1,76 @@
    +   * @return \Drupal\Core\Plugin\Context\ContextInterface[]
    +   *   The determined contexts, keyed by the unqualified context_id.
    
    +++ b/core/lib/Drupal/Core/Plugin/Context/ContextRepositoryInterface.php
    @@ -0,0 +1,40 @@
    +   * @return \Drupal\Core\Plugin\Context\ContextInterface[]
    +   *   The determined contexts.
    

    Almost consistent :) But I suspect the latter is also supposed to be keyed by context ID? That's not documented.

  6. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextProviderInterface.php
    @@ -0,0 +1,76 @@
    +   * Provides the available contexts.
    
    +++ b/core/lib/Drupal/Core/Plugin/Context/ContextRepositoryInterface.php
    @@ -0,0 +1,40 @@
    +   * Gets all available contexts for the purposes of configuration.
    

    Inconsistent.

  7. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextProviderInterface.php
    @@ -0,0 +1,76 @@
    +   * @see static::getRuntimeContext()
    

    Typo in method reference: s/getRuntimeContext()/getRuntimeContexts()/

  8. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextRepositoryInterface.php
    @@ -0,0 +1,40 @@
    + * It provides a ist 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.
    

    s/ist/list/
    80 cols.

  9. +++ b/core/lib/Drupal/Core/Plugin/Context/LazyContextRepository.php
    @@ -0,0 +1,109 @@
    + * Provides a context repository which uses context providers in the container.
    

    Can there also be context providers outside of the container?

    Sounds a bit strange.

  10. +++ b/core/modules/block/src/BlockForm.php
    @@ -69,23 +67,30 @@ class BlockForm extends EntityForm {
    +   * The context manager service.
    ...
    +  protected $contextRepository;
    

    manager vs repository

  11. +++ b/core/modules/block/src/BlockRepositoryInterface.php
    --- a/core/modules/block/src/Entity/Block.php
    +++ b/core/modules/block/src/Entity/Block.php
    
    +++ b/core/modules/block/src/Entity/Block.php
    +++ b/core/modules/block/src/Entity/Block.php
    @@ -253,21 +253,6 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) {
    
    @@ -253,21 +253,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}
    -   */
    

    Nice!

catch’s picture

Not a full review, but posting for now.

  1. +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/ContextProvidersPass.php
    @@ -0,0 +1,33 @@
    +  public function process(ContainerBuilder $container) {
    

    Off-topic but no interface to type hint on?

  2. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextProviderInterface.php
    @@ -0,0 +1,76 @@
    +  public function getRuntimeContexts(array $unqualified_context_ids);
    

    I still find runtime vs.available not entirely self-documenting. Could this be getContextsWithValues() or similar?

  3. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextRepositoryInterface.php
    @@ -0,0 +1,40 @@
    + * It provides a ist of all available contexts, which is mostly useful
    

    No need for "It"

  4. +++ b/core/lib/Drupal/Core/Plugin/Context/LazyContextRepository.php
    @@ -0,0 +1,109 @@
    +class LazyContextRepository implements ContextRepositoryInterface {
    

    Agreed with Wim that Lazy suggests there's a non-lazy.

  5. +++ b/core/lib/Drupal/Core/Plugin/Context/LazyContextRepository.php
    @@ -0,0 +1,109 @@
    +      if ($id[0] === '@' && strpos($id, ':') !== FALSE) {
    

    Could be an assert later? Or can this come from stored data?

  6. +++ b/core/modules/comment/comment.module
    @@ -345,7 +345,7 @@ function comment_entity_storage_load($entities, $entity_type) {
    +  if (!\Drupal::hasService('comment.manager') || !\Drupal::service('comment.manager')->getFields($entity_type)) {
    

    This could use a comment as to why it's necessary.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/ContextProvidersPass.php
    @@ -0,0 +1,33 @@
    +    foreach (array_keys($container->findTaggedServiceIds('context_provider')) as $id) {
    

    We should document this so it appears on https://api.drupal.org/api/drupal/core%21core.api.php/group/service_tag/8

  2. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextProviderInterface.php
    @@ -0,0 +1,76 @@
    +   * @see static::getAvailableContexts()
    ...
    +   * @see static::getRuntimeContext()
    

    @see static:: does not work - see https://api.drupal.org/api/drupal/core%21modules%21block%21src%21EventSu...

  3. +++ b/core/lib/Drupal/Core/Plugin/Context/LazyContextRepository.php
    @@ -0,0 +1,109 @@
    +    // 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;
    +      }
    +    }
    

    What happens if the service id does not exist or does not provide the context?

  4. +++ b/core/modules/block/src/Plugin/DisplayVariant/BlockPageVariant.php
    @@ -8,18 +8,15 @@
    +use Drupal\Core\Theme\ThemeManagerInterface;
    

    Not used.

  5. +++ b/core/modules/block/tests/src/Unit/Plugin/DisplayVariant/BlockPageVariantTest.php
    @@ -9,6 +9,7 @@
    +use Drupal\Core\Theme\ActiveTheme;
    

    Not used.

  6. +++ b/core/tests/Drupal/Tests/Core/Plugin/Context/LazyContextRepositoryTest.php
    @@ -0,0 +1,152 @@
    +   *   An array of context slot names.
    +   * @param string[] $expected_unqualified_context_ids
    +   *   The expected context slotes passed to getRuntimeContexts.
    

    Slotes? Also I think we stopped using slots?

dawehner’s picture

Assigned: Berdir » dawehner

Working on it

dawehner’s picture

Assigned: dawehner » Unassigned
Status: Needs work » Needs review
FileSize
60.86 KB
9.89 KB

Off-topic but no interface to type hint on?

To be clear, this is the interface of CompilerPasses and no, there is none.

Agreed with Wim that Lazy suggests there's a non-lazy.

Well, there is, page manager will have some of those. They know they contexts in advance, coming from page arguments.

Could be an assert later? Or can this come from stored data?

Sure

What happens if the service id does not exist or does not provide the context?

As talked with berdir, there are usecases for missing context values, and even context objects. I adapted the documentation to take that into account.

Berdir’s picture

  1. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextProviderInterface.php
    @@ -15,15 +15,17 @@
      * The fully qualified context ID then includes the service ID:
      * @service_id:unqualified_context_id.
    

    you should use {} here as well, then.

  2. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextProviderInterface.php
    @@ -35,19 +37,24 @@
    +   * On the other hand, there are usescases, on which roviders no longer are
    

    usecases should be use cases I think. Or just cases.

    and *p*roviders :)

  3. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextProviderInterface.php
    @@ -35,19 +37,24 @@
    -   *   The determined contexts, keyed by the unqualified context_id.
    +   *   The determined contexts, keyed by the unqualified context_id, but not
    +   *   necessarily filled up with context objects for all of them.
    

    that seems complicated.

    Mybe just add "The determined available contexts, ..." to the original docs?

  4. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextRepositoryInterface.php
    @@ -10,22 +10,28 @@
    +   * 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.
    

    The second , is not needed.

  5. +++ b/core/lib/Drupal/Core/Plugin/Context/LazyContextRepository.php
    @@ -64,6 +64,8 @@ public function getRuntimeContexts(array $context_ids) {
           // 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.
    

    two spaces on second line ;)

  6. +++ b/core/modules/comment/comment.module
    @@ -345,6 +345,7 @@ function comment_entity_storage_load($entities, $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)) {
    

    ... why the service does sometimes not exist? (to explain what the todo is actually about)

  7. +++ b/core/tests/Drupal/Tests/Core/Plugin/Context/LazyContextRepositoryTest.php
    @@ -48,6 +48,20 @@ public function testGetRuntimeContextsSingle() {
    +   * @expected \LogicException
    +   * @expected
    

    should be the message?

Status: Needs review » Needs work

The last submitted patch, 239: 2354889-239.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
FileSize
4.81 KB
60.33 KB

you should use {} here as well, then.

I had a look at the entire code base, but was simply just blind.

Mybe just add "The determined available contexts, ..." to the original docs?

Fair, we have more docs above.

... why the service does sometimes not exist? (to explain what the todo is actually about)

Well, this is what we need to investigate!

should be the message?

This was actually not intended to be added.

Wim Leers’s picture

Thanks, looks great now. Will leave to Berdir to re-RTBC.


Two tiny nits that can be fixed upon commit:

  1. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextRepositoryInterface.php
    @@ -10,22 +10,28 @@
    +   *   The determined contexts, keyed by the fully qualified context_id.
    

    Nit: s/context_id/context ID/

  2. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextProviderInterface.php
    @@ -0,0 +1,82 @@
    +   * @see \Drupal\Core\Plugin\Context\ContextProviderInterface::getRuntimeContext()
    

    I don't think this needs to include the FQCN, but it doesn't hurt of course.

Berdir’s picture

Status: Needs review » Reviewed & tested by the community
+++ b/core/lib/Drupal/Core/Plugin/Context/LazyContextRepository.php
@@ -65,7 +65,7 @@ public function getRuntimeContexts(array $context_ids) {
       // @todo Convert to an assert once https://www.drupal.org/node/2408013 is
-      //  in.
+      // in.
       if ($id[0] === '@' && strpos($id, ':') !== FALSE) {

I meant one more space, not one less :)

Can be done on commit as well.

The @see is actually correct IMHO.

Back to @alexpott/@catch then :)

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed 794be16 and pushed to 8.0.x. Thanks!

re #243.2 well static:: does not work with api.d.o so not fixing.

diff --git a/core/lib/Drupal/Core/Plugin/Context/ContextRepositoryInterface.php b/core/lib/Drupal/Core/Plugin/Context/ContextRepositoryInterface.php
index aa4edbe..6d7d609 100644
--- a/core/lib/Drupal/Core/Plugin/Context/ContextRepositoryInterface.php
+++ b/core/lib/Drupal/Core/Plugin/Context/ContextRepositoryInterface.php
@@ -31,7 +31,7 @@
    *   node.node_route_context:node.
    *
    * @return \Drupal\Core\Plugin\Context\ContextInterface[]
-   *   The determined contexts, keyed by the fully qualified context_id.
+   *   The determined contexts, keyed by the fully qualified context ID.
    */
   public function getRuntimeContexts(array $context_ids);
 
diff --git a/core/lib/Drupal/Core/Plugin/Context/LazyContextRepository.php b/core/lib/Drupal/Core/Plugin/Context/LazyContextRepository.php
index 2e08773..4e9325e 100644
--- a/core/lib/Drupal/Core/Plugin/Context/LazyContextRepository.php
+++ b/core/lib/Drupal/Core/Plugin/Context/LazyContextRepository.php
@@ -63,9 +63,10 @@ public function getRuntimeContexts(array $context_ids) {
         $contexts[$id] = $this->contexts[$id];
         continue;
       }
-      // The IDs have been passed in @{service_id}:{unqualified_context_id} format.
+      // 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.
+      //   in.
       if ($id[0] === '@' && strpos($id, ':') !== FALSE) {
         list($service_id, $unqualified_context_id) = explode(':', $id, 2);
         // Remove the leading '@'.

Minor fixes on commit - thanks @Wim Leers and @Berdir

  • alexpott committed 794be16 on 8.0.x
    Issue #2354889 by larowlan, dawehner, lauriii, Berdir, catch, martin107...
Wim Leers’s picture

Published the CR and tagged #2508884: Make contexts immutable for a reroll.

andypost’s picture

Looks language context should live in Core\Language otherwise we have useless compiler pass
Filed follow-up to discus and quickfix #2529616: Move CurrentLanguageContext to Core\Language\ContextProvider

jhodgdon’s picture

Issue summary: View changes

Fixing "after" in issue summary, since this is apparently wrong (fixing to match change record, which is correct).

  • catch committed 6f96411 on
    Issue #2528178 by dawehner, amateescu, larowlan, tim.plunkett, jibran,...
webflo’s picture

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.