Problem/Motivation

#2272801: Allow blocks to be context aware. provided blocks the ability to be context aware.
However, if a context-aware block plugin were written right now, it would appear under admin/structure/blocks, but would not actually be usable since the block system does not set context.

Plugin managers need to be able to restrict certain listings to the contexts that are available.

Additionally, if a context-aware system were actually written, there is no way to actually use those contexts to satisfy a constraint, or to prepare a plugin by setting its expected contexts.

Proposed resolution

Add a ContextHandler class and service, providing the following methods:

  • getAvailablePluginDefinitions()
  • getValidContexts()
  • preparePluginContext()
  • checkRequirements()

Add a ContextAwarePluginManagerTrait for helping plugin managers to deal with context-aware plugins

Fix the BlockManager to use these new methods

Comes with unit and integration tests.

Remaining tasks

N/A

User interface changes

N/A

API changes

N/A

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

tim.plunkett’s picture

Status: Active » Needs review
FileSize
26.55 KB

EclipseGc should also get credit on this commit, since this class is a heavily refactored version of his code.

tim.plunkett’s picture

Issue tags: -Needs tests
FileSize
2.32 KB
642 bytes
28.38 KB
28.8 KB

Okay, here's a web test showing how the unsatisfied context-aware block should be removed from the block list.

The last submitted patch, 2: context-handler-2277981-2-FAIL.patch, failed testing.

tim.plunkett’s picture

Issue summary: View changes
tim.plunkett’s picture

+++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php
@@ -0,0 +1,173 @@
+      // If any constraint does not match, this context is invalid.
+      foreach ($definition->getConstraints() as $constraint_name => $constraint) {
+        if ($context_definition->getConstraint($constraint_name) != $constraint) {
+          return FALSE;
+        }
+      }

Discussed with @eclipsegc and @fago, agreed that this is not ideal for the long run, but we'll be unable to make any progress without this, so we'll try to improve TypedData as we go.

Status: Needs review » Needs work

The last submitted patch, 5: context-handler-2277981-5.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
29.01 KB
748 bytes
fago’s picture

Issue tags: +d8rules
tim.plunkett’s picture

+++ b/core/lib/Drupal/Core/Plugin/Context/ContextAwarePluginManagerInterface.php
@@ -0,0 +1,28 @@
+   * @param \Drupal\Component\Plugin\Context\ContextInterface[] $contexts
+   *   An array of contexts.
...
+  public function getAvailablePluginDefinitions(array $contexts = array());

@fago pointed out that this really should be an array of context definitions. Nowhere in this call stack does it need the Context object or value, just the definition. This means we're blocked on #2281635: \Drupal\Core\Plugin\Context\Context should use a ContextDefinition class instead of arrays.

tim.plunkett’s picture

Status: Postponed » Needs review
FileSize
29.35 KB
2.18 KB

Actually, #2281635: \Drupal\Core\Plugin\Context\Context should use a ContextDefinition class instead of arrays is further away than I thought, and I think its valid to get this in, and switch later from Context to ContextDefinition when we can.

tim.plunkett’s picture

FileSize
29.34 KB
4.01 KB

getAvailablePluginDefinitions was a misleading name, since the word "available" does not help convey that it returns a filtered set of plugin definitions.
Since we're passing in an array of contexts to filter, after talking with @msonnabaum, @kimb0, and @fago, we've settled on getPluginDefinitionsForContexts() for the service's method.
When on the plugin manager itself, it's just getDefinitionsForContexts(), to mirror getDefinitions().

tim.plunkett’s picture

FileSize
30.03 KB
1.57 KB

Quick reroll because I left out ConditionManager, and noticed some cruft.

fago’s picture

Status: Needs review » Needs work
  1. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php
    @@ -0,0 +1,179 @@
    +   * @param \Drupal\Core\TypedData\DataDefinitionInterface[] $requirements
    

    I'm not sure requirement is the right term here as it might contain optional context as well.
    What about $contexts (described as available contexts) plus $needed_contexts ? I.e., probably the requirements move to ContextDefinitionInterface in the follow up as well.

  2. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php
    @@ -0,0 +1,179 @@
    +  public function checkRequirements(array $contexts, array $requirements) {
    

    Minor, but I'd have expected this helper to be below getPluginDefinitionsForContexts().

  3. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php
    @@ -0,0 +1,179 @@
    +  public function getPluginDefinitionsForContexts(array $contexts, array $definitions) {
    

    It was not clear to me what $definitions need be passed here exactly before reading the code. From the method name I'd have expected it to use $this->getDefinitions() internally. If they are passed trough it's actually not getting you anything, it's filtering.

  4. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php
    @@ -0,0 +1,179 @@
    +        $requirements[$context_id] = new DataDefinition($definition);
    

    That wrongly hardcodes a class; i.e. needs at least a todo to use the right class or migrate to context definition.

  5. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php
    @@ -0,0 +1,179 @@
    +  public function getValidContexts(array $contexts, DataDefinitionInterface $definition) {
    

    "Valid context" incorrectly makes me think of context that passes validation. Can we call this "getMatchingContext()"

  6. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php
    @@ -0,0 +1,179 @@
    +   * @param \Drupal\Component\Plugin\Context\ContextInterface[] $contexts
    +   *   An array of contexts to set on the plugin.
    

    Does it have to match the plugin context or not? We need to be specific of what kind of context is expected where, i.e. available or needed context or whatever we end up calling them.

  7. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php
    @@ -0,0 +1,179 @@
    +    if ($plugin instanceof ConfigurablePluginInterface) {
    +      $configuration = $plugin->getConfiguration();
    +      if (isset($configuration['context_assignments'])) {
    +        $assignments = array_flip($configuration['context_assignments']);
    +      }
    

    Instead of checking for the interface here, can we just pass in the context assignments and let the method apply them?

    -> applyContextMapping() would make sense to me.

  8. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php
    @@ -0,0 +1,179 @@
    +      $name = isset($assignments[$name]) ? $assignments[$name] : $name;
    +      if (isset($plugin_contexts[$name])) {
    +        $plugin->setContextValue($name, $context->getContextValue());
    

    This should throw an exception if something is in the assignment but it cannot be applied. Silently skipping it even might have security implications, e.g. silently skipping an optional account for which to check access...

  9. +++ b/core/tests/Drupal/Tests/Core/Plugin/ContextHandlerTest.php
    @@ -0,0 +1,379 @@
    +  public function providerTestGetAvailablePluginDefinitions() {
    

    Some methods test methods miss a doc block.

  10. +++ b/core/tests/Drupal/Tests/Core/Plugin/ContextHandlerTest.php
    @@ -0,0 +1,379 @@
    +interface TestConfigurableContextAwarePluginInterface extends ContextAwarePluginInterface, ConfigurablePluginInterface {
    

    ouch

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
31.68 KB
14.41 KB

1) The first thing we do is check if ($requirement->isRequired()). So yes, some optional things might be passed in, but the method is concerned with actual required ones, and I think this naming is fine for now.
2) Moved, makes sense.
3) Discussed with @fago, this is just filtering, there is no "getting". Renamed to filterPluginDefinitionsByContexts()
4) Add an @todo, same as the others.
5) Agreed, renamed
6) Clarified this
7) Renamed, and passed the assignments in.
8) Added an exception
9) Added.
10) It is what it is :)

fago’s picture

Status: Needs review » Needs work

Thanks, mostly some more minor stuff left:

  1. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php
    @@ -152,27 +156,33 @@ public function getValidContexts(array $contexts, DataDefinitionInterface $defin
    +   *   An array of available contexts to set on the plugin. They will only be
    

    It's not the array of context to set, it's just the available context - whatever that is :)

  2. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php
    @@ -152,27 +156,33 @@ public function getValidContexts(array $contexts, DataDefinitionInterface $defin
    +   * @param array $assignments
    

    $mapping ?

  3. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php
    @@ -152,27 +156,33 @@ public function getValidContexts(array $contexts, DataDefinitionInterface $defin
    +   *   plugin expects 'node', then this map would contain 'entity' => 'node'.
    

    Maybe, improved it to

    expects a context named 'node' just to avoid confusing people that this is not entity:node

+++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php
@@ -0,0 +1,189 @@
+class ContextHandler {

There should be an interface for that - sry for missing that earlier.

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
32.78 KB
9.16 KB

1) Just dropping the "available" part. It *is* an array of contexts, provided by the calling code. We can't make more assumptions about it.
2) Renamed.
3) Good point

And, added an interface.

fago’s picture

Status: Needs review » Reviewed & tested by the community

Thanks, changes look good. -> RTBC.

Fond one minor remark, but that should not hold us back from committing this.

+++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandlerInterface.php
@@ -0,0 +1,83 @@
+   *   An array of valid contexts.

array of matching contexts

tim.plunkett’s picture

FileSize
32.79 KB

Rerolled with that change, just to keep things easy. Thanks @fago, this code is much better than what we started with!

tim.plunkett’s picture

EclipseGc and fago should also get credit for this.

git commit -m "Issue #2277981 by tim.plunkett, EclipseGc, fago: Provide a service for handling context-aware plugins."

We started with https://gist.github.com/EclipseGc/f0cd16407d09e2362314, which was essentially copied straight from D7 CTools, and it has been refactored and polished since then.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed 47e7496 and pushed to 8.x. Thanks!

+++ b/core/modules/block/src/Tests/BlockUiTest.php
@@ -158,6 +158,24 @@ public function testCandidateBlockList() {
+  public function testContextAwareBlocks() {
+    $arguments = array(
+      ':ul_class' => 'block-list',
+      ':li_class' => 'test-context-aware',
+      ':href' => 'admin/structure/block/add/test_context_aware/stark',
+      ':text' => 'Test context-aware block',
+    );
+
+    $this->drupalGet('admin/structure/block');
+    $elements = $this->xpath('//details[@id="edit-category-block-test"]//ul[contains(@class, :ul_class)]/li[contains(@class, :li_class)]/a[contains(@href, :href) and text()=:text]', $arguments);
+    $this->assertTrue(empty($elements), 'The context-aware test block does not appear.');
+    $definition = \Drupal::service('plugin.manager.block')->getDefinition('test_context_aware');
+    $this->assertTrue(!empty($definition), 'The context-aware test block exists.');
+  }

I think the $this->assertTrue(empty($elements), 'The context-aware test block does not appear.'); is more than a little bit brittle but we can fix that when it bites us back.

  • Commit 47e7496 on 8.x by alexpott:
    Issue #2277981 by tim.plunkett, EclipseGc, fago: Provide a service for...
tim.plunkett’s picture

Assigned: tim.plunkett » Unassigned

Thanks!

Status: Fixed » Closed (fixed)

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