Problem/Motivation

#2459819: Remove CacheableInterface (and no longer let block plugins implement it) introduced RendererInterface::addDependency(array $build, CacheableDependencyInterface $object). That's great.

But many objects can optionally implement CacheableDependencyInterface. Which means that in many cases, before calling ::addDependency(), we need to check that the depended object actually does implement that interface. Which makes the DX atrocious. We absolutely need this DX to be super simple.

This is the case for field definition objects for example: some are config entities (and thus implement CacheableDependencyInterface), others aren't (and thus don't). But the best example are access results: AccessResultInterface is the only required interface, but most (though not all!) access result objects actually also implement CacheableDependencyInterface. For the code to be correct, again, we currently need to do an if-test before calling ::addDependency(). That's terrible.

Proposed resolution

Remove addDependency()'s typehint and do an instanceof CacheableDependencyInterface instead.

Remaining tasks

None.

User interface changes

None.

API changes

API relaxation, no breaking API change.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Wim Leers’s picture

Wim Leers’s picture

Issue tags: +blocker
+++ b/core/lib/Drupal/Core/Render/BubbleableMetadata.php
@@ -109,18 +109,26 @@ public static function createFromRenderArray(array $build) {
+    // Objects that don't implement CacheableDependencyInterface must be assumed
+    // to be uncacheable, so set max-age 0.
     $meta = new static();
...
+    $meta->maxAge = 0;

This is problematic, because it doesn't apply to everything.

E.g.:

  1. in the issue that spawned this issue (#2099137), we need this behavior for AccessResultInterface objects that aren't cacheable.
  2. but e.g. in EntityFormDisplay, we are currently still doing this:
            $field_definition = $this->getFieldDefinition($name);
            if ($field_definition instanceof CacheableDependencyInterface) {
              $this->renderer->addDependency($form[$name], $field_definition);
            }
    

    We could remove that if-statement too, but then we'd effectively be setting max-age = 0 on this form, which doesn't make sense. Because in this case, we have e.g. BaseFieldDefinition, which is a never-changing object. Which means max-age = 0 is absolutely not what we want: the form should still remain cacheable!

The difference between the two examples is that the access result is dynamically calculated (and is therefore not cacheable without it providing cacheability metadata, i.e. without implementing CacheableDependencyInterface). But the field definition is hardcoded, and is thus permanently cacheable.

The question is: how do we fix this? The simplest possible solution is to special case AccessResultInterface to get max-age = 0. The architecturally more sound choice would be to add a DynamicallyCalculatedInterface or UncacheableByDefaultInterface and let AccessResultInterface extend that. But that seems like a whole can of worms.

Any ideas?

Fabianx’s picture

"- The simplest possible solution is to special case AccessResultInterface to get max-age = 0."

Yes, lets do that. This matches the code that was there before.

( btw. funny side effect: I thought of _exactly_ that scenario when reading the IS, then saw that you had the same thought and my solution was the same as above.)

Lets keep it simple.

Wim Leers’s picture

Assigned: Unassigned » Wim Leers
Status: Needs review » Needs work

( btw. funny side effect: I thought of _exactly_ that scenario when reading the IS, then saw that you had the same thought and my solution was the same as above.)

Hah! :)

Will do that, then.

effulgentsia’s picture

I disagree with special casing AccessResultInterface. If we call addDependency(), it's because we depend on the object, and if that object can't tell us its cacheability metadata, we need to assume it's not cacheable.

Because in this case, we have e.g. BaseFieldDefinition, which is a never-changing object.

If we want to remove the wrapping if statement of #2.2, then let's do it by making BaseFieldDefinition implement CacheableDependencyInterface. Perhaps we can have a trait that implements the 3 methods to return no contexts, no tags, and permanent age?

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
8.08 KB
2.11 KB

Interesting. In #2, I only looked at it from one angle. #5 basically presents the inverse angle. #5 basically says that we should set max-age = 0 if CacheableDependencyInterface isn't implemented. And it says BaseFieldDefinition should simply implement CacheableDependencyInterface. Which makes a lot of sense.

The implication of #5 is: if your page/render array is uncacheable because of a dependency on an uncacheable object that actually is cacheable, then make that object implement CacheableDependencyInterface.

It's hard to argue with that, since it just means that all things that can implement CacheableDependencyInterface should.

Fabianx’s picture

+++ b/core/lib/Drupal/Core/Cache/UnchangingCacheableDependencyTrait.php
@@ -0,0 +1,38 @@
+trait UnchangingCacheableDependencyTrait {

+++ b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php
@@ -16,7 +18,9 @@
+  use UnchangingCacheableDependencyTrait;

Love the idea, but the name is debatable.

What about:

PermanentCacheableDependencyTrait?

---

Besides "naming things": This is really great! Lets get this in!

Wim Leers’s picture

I think "permanent" doesn't capture as many semantics as "unchanging". "permanent" clearly alludes to max-age, but "unchanging" also points out that there are no invalidations (no cache tags) and no variations (no cache contexts), and that it can thus be permanently cached (max-age == permanent).

Fabianx’s picture

Status: Needs review » Reviewed & tested by the community

RTBC then, can't think of a better name.

effulgentsia’s picture

Unchanging works for me. RTBC+1. Synonyms for "unchanging" include "static" and "constant", but I'm torn on whether either would be an improvement given that those words already have additional specific meaning in PHP.

Wim Leers’s picture

#10: exactly!

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs tests
+++ b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php
@@ -16,7 +18,9 @@
+  use UnchangingCacheableDependencyTrait;

This is missing tests afaics

Wim Leers’s picture

+++ b/core/lib/Drupal/Core/Cache/UnchangingCacheableDependencyTrait.php
@@ -0,0 +1,38 @@
+trait UnchangingCacheableDependencyTrait {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheTags() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    return Cache::PERMANENT;
+  }
+
+}

There is nothing to test. This is just a trait to implement 3 interface methods declaratively, with zero logic.

See #8 and #10 for why this name was chosen. The return values match the semantics of the word "unchanging".

Wim Leers’s picture

Status: Needs work » Reviewed & tested by the community
Issue tags: -Needs tests
FileSize
6.08 KB
2.07 KB

#12 pointed out that we're modifying BaseFieldDefinition, but not properly explaining why.

  • #2463029: EntityFormDisplay should update $form with cache tags of FieldConfig, FieldStorageConfig, EntityFormDisplay config entities had to check if the field definition & field storage definition implement CacheableDependencyInterface, because you can only call RendererInterface::addDependency with objects that implement that interface. The goal of this issue is to remove that requirement: any object can be passed, but objects that don't implement that interface are automatically considered uncacheable.
  • To that I remarked in #2.2 that it's not quite correct, because e.g. BaseFieldDefinition doesn't implement CacheableDependencyInterface yet is still cacheable. To which #5 said, and what this patch does: let's make that then implement that interface! And add a trait at the same time, because many things in Drupal are defined in code like that and therefore have unchanging cacheability.

Clearly, these are simply separate problems! I'm moving the trait and the changes to BaseFieldDefinition into its own issue. Did that: #2467627: Field(Storage)DefinitionInterface should implement CacheableDependencyInterface.

Re-uploading this patch without the trait and without the changes to BaseFieldDefinition, restoring to RTBC state.

Fabianx’s picture

RTBC + 1

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 a4e51c8 and pushed to 8.0.x. Thanks!

  • alexpott committed a4e51c8 on 8.0.x
    Issue #2464877 by Wim Leers: Update RendererInterface::addDependency()...
Wim Leers’s picture

Yay! This unblocked, now rerolled without this patch included: #2099137-157: Entity/field access and node grants not taken into account with core cache contexts.

Also updated the CR at https://www.drupal.org/node/2460819.

Wim Leers’s picture

Status: Fixed » Closed (fixed)

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