Problem/Motivation

This is blocking #2375695: Condition plugins should provide cache contexts AND cacheability metadata needs to be exposed as it prevents that issue from getting cacheable metadata when context is missing.

When defining the context that a plugin needs, it can be defined as required (default) or optional.

The expectation is that the ContextHandler does check the required information and does not result in an array when a context is optional.

Right now, this information is not checked at all. ContextHandler just calls getContextValue(), which throws an exception when there is no value and the context is required.

The problem is, getContextValue() is called on the context object that is *provided*, not the one from the plugin (Obviously). But that is also obviously wrong. The current user context that is provided should not even have the concept of being required or not. It's just there.

Proposed resolution

ContextHandler needs to check the context definition of the context object of the plugin. If it's optional and no context is available, then we do not need to throw an exception.

The problem with that is that there is no way to do that, there is no method to check if a Context object *has* a value. There are a number of ways to do this, likely we actually want to do multiple things if not all.

a) We can add a hasContextValue() method. Makes sense anyway conceptually, and we did that with plugin managers too.
b) We can remove the thrown exceptions in getContextValue(). I think they should not exist, it just doesn't make sense to me, but if we have the hasMethods() then we can also leave them be.
c) Force all provided contexts to not be required. It would be even better if they wouldn't be able to be required or optional at all I guess that would require different definition objects which would be a considerably definition object.

Remaining tasks

User interface changes

API changes

Data model changes

Comments

berdir’s picture

Status: Active » Needs review
StatusFileSize
new12.09 KB

Extracting the relevant parts from #2375695: Condition plugins should provide cache contexts AND cacheability metadata needs to be exposed, let's see what happens.

Probably needs some integration tests as well and not just unit tests.

berdir’s picture

wim leers’s picture

Looks good.

Just a small remark about the code structure.

  1. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php
    @@ -74,14 +74,35 @@ public function getMatchingContexts(array $contexts, ContextDefinitionInterface
    +        // Collect contexts that exist but are missing a value.
    +        if ($plugin_context_definition->isRequired() && !$contexts[$context_id]->hasContextValue()) {
    +          $missing_value[] = $plugin_context_id;
    +          continue;
    +        }
    +
    +        if ($contexts[$context_id]->hasContextValue()) {
    +          $plugin->setContextValue($plugin_context_id, $contexts[$context_id]->getContextValue());
    +        }
    

    Wouldn't "if/else" be clearer then "if,continue,if"?

  2. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php
    @@ -74,14 +74,35 @@ public function getMatchingContexts(array $contexts, ContextDefinitionInterface
    +        continue;
    

    This "continue" seems similarly unnecessary.

berdir’s picture

You're right, that can be simplified.

Added some kernel tests, to have some useful failing tests. Found that I missed one case before, I also need to unset the mapping if a context is optional and completely missing.

berdir’s picture

Note. Unlike the implementation in #2375695: Condition plugins should provide cache contexts AND cacheability metadata needs to be exposed, thhis currently only implements a). Not sure what to do about b/c yet.

eclipsegc’s picture

Status: Needs review » Needs work
+++ b/core/lib/Drupal/Component/Plugin/Context/Context.php
@@ -70,6 +70,13 @@ public function getContextValue() {
+    return $this->contextValue || $this->getContextDefinition()->getDefaultValue();

+++ b/core/lib/Drupal/Core/Plugin/Context/Context.php
@@ -59,6 +59,13 @@ public function getContextValue() {
+    return $this->contextData || parent::hasContextValue();

Need some explicit (bool) castings.

Worth noting that berdir's code tip-toes around the plugin's implementation of setContextValue() because that method can throw an exception. It needs to continue to throw that exception in the case of someone not using the ContextHandler to set contexts on an object, so I approve whole-heartedly of the approach here. ++ all around, change the boolean problems and I'm very on board.

Eclipse

berdir’s picture

Status: Needs work » Needs review
StatusFileSize
new16.19 KB
new1.07 KB

The || actually forces the return value to a bool, but I wasn't sure myself and had to test it. So, making that clear in the code doesn't hurt I think.

The last submitted patch, 4: context-handler-required-2513244-3-test-only.patch, failed testing.

berdir’s picture

Messed up the test-only patch.

Status: Needs review » Needs work

The last submitted patch, 9: context-handler-required-2513244-9-test-only.patch, failed testing.

berdir’s picture

Status: Needs work » Needs review

Failed as expected.

wim leers’s picture

Here's a review with minor stuff. This looks great to me, but I think only EclipseGc/tim.plunkett can RTBC this. At least definitely not me.

  1. +++ b/core/lib/Drupal/Component/Plugin/Context/Context.php
    @@ -70,6 +70,13 @@ public function getContextValue() {
    +    return (bool) $this->contextValue || (bool) $this->getContextDefinition()->getDefaultValue();
    

    Nit: I'd have expected

    (bool) (A || B)
    
  2. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php
    @@ -74,14 +74,36 @@ public function getMatchingContexts(array $contexts, ContextDefinitionInterface
    +    // if there are any required contexts without a value, throw an exception.
    

    Nit: s/if/If/

  3. +++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php
    @@ -74,14 +74,36 @@ public function getMatchingContexts(array $contexts, ContextDefinitionInterface
    +      throw new ContextException(SafeMarkup::format("Required contexts without a value: @contexts.", array('@contexts' => implode(', ', $missing_value))));
    

    We use sprintf() for these.

    (Yes, much code is doing this incorrectly, but Alex Pott will push it back for that.)

  4. +++ b/core/modules/system/tests/modules/condition_test/src/Plugin/Condition/OptionalContextCondition.php
    @@ -0,0 +1,40 @@
    + *     "node" = @ContextDefinition("entity:node", label = @Translation("Node"), required = FALSE),
    ...
    +class OptionalContextCondition extends ConditionPluginBase {
    

    Name mismatch?

berdir’s picture

1. I don't think that makes sense. As written above, || actually forces the return value to bool already, we just added the type casts to make it really obvious. Your suggestion would cast bool to bool.

3. Always the same problem then, should we also update the existing call below? I hate those exceptions and would love to remove them completely but that's a different discussion.

4. Discussed, wasn't sure what mismatch you mean exactly. It's OptionalContext vs. a specific context type (entity:node). I kind of like my name and I chose node because we actually have an optional context for them, so we could do a real web tests with block conditions for it. To be honest, I'd prefer to keep it, maybe add a comment why we use that. Would make it easier to write web tests for it in case we ever need that. Other opinions?

wim leers’s picture

  1. Ok.
  2. \
  3. I think in this case it's fine to just use sprintf() for this one. It's not as jarring as seeing different array syntax.
  4. A comment would be okay too probably. But right now it's just confusing.
dsnopek’s picture

Issue tags: +D8panels
berdir’s picture

berdir’s picture

I hope that comment is enough to address 4. :)

eclipsegc’s picture

Status: Needs review » Reviewed & tested by the community

Looks good to me and is passing. This is a big oops, the sooner this is fixed the better.

Eclipse

tim.plunkett’s picture

+++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php
@@ -74,14 +74,36 @@ public function getMatchingContexts(array $contexts, ContextDefinitionInterface
+      throw new ContextException(sprintf('Required contexts without a value: %s.', implode(', ', $missing_value)));

This method already has @throws, so we're good here.

The unit and KTB tests are also looking good.

+1 for RTBC!

fabianx’s picture

+++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php
@@ -74,14 +74,36 @@ public function getMatchingContexts(array $contexts, ContextDefinitionInterface
       if (!empty($contexts[$context_id])) {
         // This assignment has been used, remove it.
         unset($mappings[$plugin_context_id]);
-        $plugin->setContextValue($plugin_context_id, $contexts[$context_id]->getContextValue());
+
+        // Collect contexts that exist but are missing a value.
+        if ($plugin_context_definition->isRequired() && !$contexts[$context_id]->hasContextValue()) {
+          $missing_value[] = $plugin_context_id;
+        }
+        elseif ($contexts[$context_id]->hasContextValue()) {
+          $plugin->setContextValue($plugin_context_id, $contexts[$context_id]->getContextValue());
+        }
       }
+      elseif ($plugin_context_definition->isRequired()) {
+        $missing_value[] = $plugin_context_id;
+      }
+      else {
+        // Ignore mappings for optional missing context.
+        unset($mappings[$plugin_context_id]);
+      }

Uber nit - As the logic is now the $contexts[$context_id]->hasContextValue() will be executed two times.

Using:

if ($contexts[$context_id]->hasContextValue()) {
  $plugin->set...
}
elseif ($plugin_context_definition->isRequired()) {
}

would avoid that.

----

Written that the whole logic can be even more simplified:

- Move unset() of the mappings out of condition.

=>

 if (!empty($contexts[$context_id]) && $contexts[$context_id]->hasContextValue()) {
  $plugin->setContextValue(...);
}
elseif (plugin_context_definition->isRequired()) {
  $missing_value[] = ...;
}
unset($mappings[$plugin_context_id]);

That should make that code way simpler to read ...

Leaving at RTBC as a pure refactoring this could be follow-up.

Also: RTBC + 1

berdir’s picture

@Fabianx: The problem with your suggestion is that the key in $mappings must not be unset when the context doesn't exist and is required. Also, #2375695: Condition plugins should provide cache contexts AND cacheability metadata needs to be exposed will need to add the cacheability logic in case the context exists, even if there is no value. One goal of this issue/patch was to make the additional changes there as small as possible.

fabianx’s picture

Okay, could we still try to avoid calling ->hasContextValue() twice? (e.g. the first suggestion)

berdir’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new16.25 KB
new2.04 KB

Yes, that should work. Like this?

fabianx’s picture

Status: Needs review » Reviewed & tested by the community

Back to RTBC, thanks for fixing!

eclipsegc’s picture

Doesn't check if it's required if it's present +1 to rtbc.

Eclipse

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

This issue addresses a major bug and is allowed per https://www.drupal.org/core/beta-changes. Committed 05aa5ea and pushed to 8.0.x. Thanks!

  • alexpott committed 05aa5ea on 8.0.x
    Issue #2513244 by Berdir, Wim Leers, EclipseGc, Fabianx: ContextHandler...
klausi’s picture

Interesting, this broke the Rules unit tests for node conditions: https://travis-ci.org/fago/rules/builds/69448449

Thanks to git bisect I was able to trace it down to this issue.

Not sure what is going on at all here since https://github.com/fago/rules/blob/8.x-3.x/tests/src/Integration/Conditi... only makes use of setContextValue() and nothing else. What I found so far is that the ->isPromoted() method on my mocked node has already been called twice when the condition is evaluated, which does not make sense. Will report back here once I make progress in tracking this down.

klausi’s picture

Actually the test fails started with the commits from #2375695: Condition plugins should provide cache contexts AND cacheability metadata needs to be exposed.

This is now fixed in Rules: https://github.com/fago/rules/commit/cfa9337d05d5b7944d50a372d33e14d14a5...

Moral of the story: don't use $this->at() in a PHPUnit test unless you know what you are doing. It can behave in unexpected ways. In our case additional methods are called on the node mock object ("getCacheContexts" and others) which PHPUnit matched to the configured method calls with ->at().

Status: Fixed » Closed (fixed)

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