Problem/Motivation

Discovered in #2930028-42: Comprehensive JSON API integration test coverage phase 1: for every entity type, individual resources only.

#2626924: Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata is where for the first time a computed field was marked non-internal and started showing up in core. Infrastructure support in core was added in #2910211: Allow computed exposed properties in ComplexData to support cacheability..

This issue is therefore similar to #2910211.

Proposed resolution

Automatically bubble cacheability metadata on field properties.

Detailed analysis/solution/rationale:

  1. The root cause is that core's serialization system doesn't allow cacheability to be associated with the return value of normalizers. So it needs to pass them out of band, via $context['some key']. But in JSON API, thanks to its returning value objects that can carry cacheability, that doesn't need to be the case! In JSON API's normalizers, we don't need that brittle and hard to debug approach that core uses! (See #25 and #2923779: Normalizers violate NormalizerInterface.)
  2. So, rather than making all of JSON API's normalizers behave like core's, it's better to keep them working as intended (and bubble up cacheability via the value objects returned by JSON API's normalizers), and only in the place where we exit JSON API normalizers and enter core normalizers (FieldItemNormalizer), we should catch that out-of-band bubbled cacheability, and then capture/store/pass it via the created/returned value object (FieldItemNormalizerValue). From there, it is then passed up the tree of value objects correctly!
  3. Except … that JSON API doesn't actually use this specific design goal to its full advantage. It keeps the value objects mutable, meaning that at any point, any code can modify the cacheability. Since this issue is about fixing cacheability for JSON API, it's important that we remove this mutability (RefinableCacheableDependencyInterface) and instead make the value objects immutable wrt cacheability (CacheableDependencyInterface), otherwise we risk introducing severe bugs in this area in the future.
  4. To truly prevent these sorts of problems from reappearing in the future, we should also completely remove JSON API's use of $context['cacheable_metadata'], and rely solely on value objects.

    Sadly, we can't really complete this refactor yet because we lack solid test coverage for e.g. includes (this is being worked on in #2945093: Comprehensive JSON API integration test coverage phase 3: test JSON API-specific use cases: related/relationship routes, includes and sparse field sets). For example \Drupal\jsonapi\Normalizer\RelationshipItemNormalizer::normalize() specifically deals with includes and still uses $context['cacheable_metadata'].

    So, removing $context['cacheable_metadata'] is definitely out of scope for this issue. Created #2948666: Remove JSON API's use of $context['cacheable_metadata'] for that. This issue only refactors those bits that we do have great test coverage for: normalizing of fields and field items.

Remaining tasks

None.

User interface changes

None.

API changes

None.

Data model changes

None.

CommentFileSizeAuthor
#72 2940342-72.patch82.06 KBwim leers
#72 interdiff-70-72.txt1.1 KBwim leers
#71 2940342-70.patch82.07 KBwim leers
#71 interdiff-69-70.txt1.75 KBwim leers
#69 2940342-69.patch81.84 KBwim leers
#69 interdiff-68-69.txt992 byteswim leers
#68 2940342-68.patch81.17 KBwim leers
#68 interdiff-66-68.txt2.08 KBwim leers
#66 2940342-66.patch80.14 KBwim leers
#66 interdiff-62-66.txt6.96 KBwim leers
#62 2940342-62.patch76.93 KBwim leers
#62 interdiff-57-62.txt4.87 KBwim leers
#57 2940342-57.patch76.15 KBwim leers
#57 interdiff-56-57.txt922 byteswim leers
#56 2940342-56.patch76.15 KBwim leers
#56 interdiff-55-56.txt11.97 KBwim leers
#55 2940342-55.patch64.35 KBwim leers
#55 interdiff-54-55.txt5.45 KBwim leers
#54 2940342-54.patch63.01 KBwim leers
#54 interdiff-52-54.txt3.09 KBwim leers
#52 2940342-52.patch62.26 KBwim leers
#52 interdiff-49-52.txt4.79 KBwim leers
#51 2940342-49-really.patch60.54 KBwim leers
#49 2940342-49.patch53.54 KBwim leers
#49 interdiff-47-49.txt7.07 KBwim leers
#47 2940342-47.patch46.45 KBwim leers
#47 interdiff-45-47.txt7.14 KBwim leers
#45 2940342-45.patch46.45 KBwim leers
#45 interdiff-43-45.txt8.63 KBwim leers
#43 2940342-43.patch40.19 KBwim leers
#43 interdiff-41-43.txt1.1 KBwim leers
#41 2940342-41.patch40.15 KBwim leers
#41 interdiff-39-41.txt10.24 KBwim leers
#39 interdiff-37-39.txt701 byteswim leers
#39 2940342-39.patch34.21 KBwim leers
#37 2940342-37.patch33.54 KBwim leers
#37 interdiff-35-37.txt2.17 KBwim leers
#35 2940342-35.patch33.68 KBwim leers
#35 interdiff-34-35.txt5.19 KBwim leers
#34 2940342-34.patch29.28 KBwim leers
#34 interdiff-33-34.txt5.59 KBwim leers
#33 2940342-33.patch29.46 KBwim leers
#33 interdiff-32-33.txt11.59 KBwim leers
#32 2940342-32.patch21.07 KBwim leers
#32 interdiff-31-32.txt9.85 KBwim leers
#31 2940342-31.patch14.6 KBwim leers
#31 interdiff-29-31.txt2.38 KBwim leers
#29 2940342-29.patch13.32 KBwim leers
#29 interdiff-28-29.txt4.14 KBwim leers
#28 2940342-28.patch9.24 KBwim leers
#28 interdiff-27-28.txt1.98 KBwim leers
#27 2940342-27.patch8.17 KBwim leers
#27 interdiff-26-27.txt2.96 KBwim leers
#26 2940342-26.patch5.26 KBwim leers
#23 2940342-23.patch28.05 KBwim leers
#23 interdiff-21-23.txt11.97 KBwim leers
#21 2940342-21.patch16.25 KBwim leers
#21 interdiff-19-21.txt920 byteswim leers
#19 2940342-19.patch16.26 KBwim leers
#19 interdiff-17-19.txt3.59 KBwim leers
#17 interdiff-16-17.txt9.13 KBwim leers
#17 2940342-17.patch15.5 KBwim leers
#16 2940342-16.patch17.53 KBwim leers
#16 interdiff-14-16.txt5.26 KBwim leers
#14 2940342-14.patch12.32 KBwim leers
#14 interdiff-5-14.txt1.94 KBwim leers
#6 2940342-5.patch10.21 KBgabesullice
#6 2940342-5.combined.patch12.4 KBgabesullice
#6 2940342-5.tests_only.patch6.71 KBgabesullice
#4 2940342-4.combined.patch5.83 KBgabesullice
#4 2940342-4.patch3.64 KBgabesullice
#4 2940342-4.tests_only.patch3.64 KBgabesullice

Comments

Wim Leers created an issue. See original summary.

gabesullice’s picture

Assigned: Unassigned » gabesullice

Yoink! I'll take this.

wim leers’s picture

👍

gabesullice’s picture

Title: Cacheability metadata on an entity fields' properties is lost » [PP-1] Cacheability metadata on an entity fields' properties is lost
Status: Active » Needs review
Issue tags: +blocked
Related issues: +#2921257: On Drupal 8.5, JSON API should respect Typed Data's new ::isInternal(), and JSON API Extras should use ::setInternal()
StatusFileSize
new3.64 KB
new3.64 KB
new5.83 KB

Status: Needs review » Needs work

The last submitted patch, 4: 2940342-4.combined.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

gabesullice’s picture

Status: Needs work » Needs review
StatusFileSize
new6.71 KB
new12.4 KB
new10.21 KB

Just ignore #4, I was missing an entire commit.

The last submitted patch, 6: 2940342-5.combined.patch, failed testing. View results

The last submitted patch, 6: 2940342-5.tests_only.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 6: 2940342-5.patch, failed testing. View results

gabesullice’s picture

Title: [PP-1] Cacheability metadata on an entity fields' properties is lost » [PP-2] Cacheability metadata on an entity fields' properties is lost
Status: Needs work » Postponed
wim leers’s picture

wim leers’s picture

Title: [PP-2] Cacheability metadata on an entity fields' properties is lost » [PP-1] Cacheability metadata on an entity fields' properties is lost
wim leers’s picture

Title: [PP-1] Cacheability metadata on an entity fields' properties is lost » Cacheability metadata on an entity fields' properties is lost
Status: Postponed » Needs review
Issue tags: -blocked
StatusFileSize
new1.94 KB
new12.32 KB

Actually, AFAICT this is not blocked on #2926463: [>=8.5] Remove JSON API's "file URL" field work-around now that Drupal core 8.5 fixed it.

Surprisingly, #5 still applied!

Status: Needs review » Needs work

The last submitted patch, 14: 2940342-14.patch, failed testing. View results

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new5.26 KB
new17.53 KB
wim leers’s picture

StatusFileSize
new15.5 KB
new9.13 KB
+++ b/src/EntityToJsonApi.php
@@ -102,7 +103,7 @@ class EntityToJsonApi {
-      'cacheable_metadata' => new CacheableMetadata(),
+      CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY => new CacheableMetadata(),

This means this only works on Drupal >=8.5. So let's just hardcode the string rather than using the constant, since the constant won't be available. (Since this bit of the patch was originally written, this was officially marked as an internal implementation detail anyway, per jsonapi.api.php.)

Status: Needs review » Needs work

The last submitted patch, 17: 2940342-17.patch, failed testing. View results

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new3.59 KB
new16.26 KB

Status: Needs review » Needs work

The last submitted patch, 19: 2940342-19.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new920 bytes
new16.25 KB

I made a mistake in #19 😅

(And apparently introduced two new CS violations 😅)

wim leers’s picture

Assigned: gabesullice » wim leers

I'm still working on this. I should've assigned it to myself some time ago. Sorry. More updates soon.

wim leers’s picture

StatusFileSize
new11.97 KB
new28.05 KB

Many of the JSON API integration tests include this:

  // @codingStandardsIgnoreStart
  /**
   * {@inheritdoc}
   */
  protected function getExpectedCacheContexts() {
    // @todo Uncomment first line, remove second line in https://www.drupal.org/project/jsonapi/issues/2940342.
//    return ['user.permissions'];
    return parent::getExpectedCacheContexts();
  }
  // @codingStandardsIgnoreEnd

They obviously refer to this issue. I copied those over from core's REST tests in #2930028: Comprehensive JSON API integration test coverage phase 1: for every entity type, individual resources only and #2932031: Comprehensive JSON API integration test coverage phase 2: for every entity type, complex use-cases. Because they were cacheability related, I pointed to this issue as being the one in which it should be solved. But the changes that this issue made so far are not actually those tests to fail … which means that either this issue should remove those @todos because they're inaccurate, or this issue should update the issue link.

So, let's dig in.

As of #2113345: Define a mechanism for custom link relationships, core's REST tests are using ['url.site', 'user.permissions'] as the default cache context expectation. Because that issue by default is generating Link response headers for all link relations for an entity type. And those headers contain absolute URLs, and hence need to vary by url.site. Okay, great.

Many config entity types only have a single "admin" permission for all possible operations. But some config entity types have per-operation permissions. In the latter case, no Link response headers will appear, because the user does not have the permission to access those other operations. Therefore in those cases, the url.site cache context should not be present.

And that is what those TODOs are about that I gave an example of. But in the land of JSON API, this is no longer true: JSON API does not expose all of an entity type's link relations via headers. But it does include a self link in every response. Therefore every JSON API response varies by the url.site cache context. Therefore it's safe to remove those @todos!

There are two special cases:

  1. File: it's the one content entity type that got similar behavior as some config entity types, because it doesn't have any link relation. But in JSON API it too always has a self link, because it's a JSON API-provided/generated link.
  2. ContentLanguageSettings: no link relations so no url.site in core REST, but it does always vary by interface language. In JSON API, it still varies by interface language, but there is always a self link and hence the url.site cache context is present

As of this patch there's zero remaining references to this issue in the code base!

wim leers’s picture

wim leers’s picture

Status: Needs review » Needs work
Related issues: +#2923779: Normalizers violate NormalizerInterface

So the patches so far have been using the pattern in Drupal core's serialization and rest modules. That pattern is:

  1. oh, Symfony's serialization component is COMPLETELY unaware of cacheability, and doesn't care at all
  2. So we have to fit in support for cacheability bubbling somehow.
  3. The only way we can possibly do this without breaking BC is by using the $context that normalizers receive, and reserve a key in there, and bubble up via that.

By making JSON API use this same pattern, it's easy for the fixed/improved normalizers in Drupal 8.5 and later (for example #2626924: Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata) to bubble up cacheability of normalizations, for both the REST and the JSON API module. So that's what the patch does so far.

However …

#2946968: Field-level cacheability metadata is lost was reported and unpublished because I needed to investigate that it wasn't a security issue. It isn't. It's the exact same bug as the one reported here. But it proposes a completely different fix.

That completely different fix points to some code that is obviously broken in \Drupal\jsonapi\Normalizer\EntityNormalizer::normalize():

    $output = new EntityNormalizerValue($normalizer_values, $context, $entity, $link_context);
    // Add the entity level cacheability metadata.
    $output->addCacheableDependency($entity);
    $output->addCacheableDependency($output);
    // Add the field level cacheability metadata.
    array_walk($normalizer_values, function ($normalizer_value) {
      if ($normalizer_value instanceof RefinableCacheableDependencyInterface) {
        $normalizer_value->addCacheableDependency($normalizer_value);
      }
    });

They pointed to the loop's logic being nonsensical (which it is). But also that $output->addCacheableDependency($output); makes no sense at all. And honestly, it also makes no sense to have $output->addCacheableDependency($entity); because the constructor of $output actually already received $entity

More importantly though, this got me thinking about the approach this issue is taking. It's applying the core approach to JSON API's normalizers. But JSON API's normalizers are NOT like core's normalizers. Because JSON API's normalizers don't return arrays, but value objects. We had a big issue specifically about that about a month ago: #2923779: Normalizers violate NormalizerInterface.

Those value objects returned by normalizers are also specifically designed to carry cacheability! Which means that the approach used in this issue is not using one of the key features that the JSON API module's architecture was designed around! This makes no sense.

What we should be doing is:

  1. use JSON API's intended architecture instead of using $context['cacheability'] at every layer like core REST
  2. only use $context['cacheability'] at the boundary: at the point(s) where we transition from JSON API's normalizers to core's serialization module style normalizers — and don't bubble this up, but merge that data into the JSON API normalizer value object that is capable of carrying cacheability

Theoretically, we can do that in a separate issue. But then we're just further postponing this much-needed clean-up. The JSON API security release from last week was related to incorrect cacheability handling. The lack of clear/consistent handling of that is clearly the root cause here. So let's fix the root cause.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new5.26 KB

So, per #25, starting a new patch.

Starting with test coverage that we'll need to make pass but that should currently fail: the test coverage expectations changes in #16.

Note that we'll be first testing these patches against 8.6 only, because it's only in 8.6 that we have #2626924: Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata.

wim leers’s picture

StatusFileSize
new2.96 KB
new8.17 KB

The absolute minimum set of changes to implement #25 is this. This should make the BlockContentTest expectations that were updated in #26 pass tests.

wim leers’s picture

StatusFileSize
new1.98 KB
new9.24 KB

Rather than making it the responsibility of the caller, it should be the responsibility of the value object itself to return the cacheability of the data it contains/depends on. And this is easily achievable!

wim leers’s picture

StatusFileSize
new4.14 KB
new13.32 KB

Including the test coverage @gabesullice wrote at #5: https://www.drupal.org/files/issues/2940342-5.tests_only.patch. But without the CacheableNormalizer:: SERIALIZATION_CONTEXT_CACHEABILITY bits, since that's no longer in this patch.

wim leers’s picture

The best way to prevent problems/regressions like this is by removing the architectural aspects that allow for these mistakes to happen:

  1. the use of $context['cacheable_metadata'] — this allows one to not use the value objects they were designed to do: carry values to be absorbed into a larger structure at a later time, plus the cacheability metadata associated with those larger structures
  2. the use of RefineableCacheableDependencyInterface — this allows one to go and modify the cacheability of constructed value objects at a later time, i.e. it makes the value objects mutable rather than immutable, making it much harder to figure out where/at what time the cacheability metadata is being specified (it can be modified at any time!)

Point 1 describes the original approach (until before #25). Point 2 is what allowed for the bug/problem being fixed in #27, and being properly fixed in #28.

If we take #28 one step further, then we prevent that problem altogether.

Significantly expanded the "proposed resolution" in the IS.

wim leers’s picture

StatusFileSize
new2.38 KB
new14.6 KB

So the goal is to get rid of $context['cacheable_metadata'], and to get to that point, we must first ensure that every value object carries the cacheability for the data it contains (i.e. the data it depends on).

#28 already made EntityNormalizerValue self-contained. But it still is open for outside change: it is still mutable. Let's make it immutable by switching from RefinableCacheableDependencyInterface to CacheableDependencyInterface.

wim leers’s picture

StatusFileSize
new9.85 KB
new21.07 KB

#31 made EntityNormalizerValue self-contained wrt cacheability.

Let's do the same for Field(Item)NormalizerValue(Interface). This:

  1. updates FieldItemNormalizerValue, FieldItemNormalizerValueInterface to implement CacheableDependencyInterface, not RefinableCacheableDependencyInterface
  2. moves the "capturing" of out-of-band bubbled cacheability by core's serialization system from FieldNormalizer to FieldItemNormalizer, because that's the actual level where we cross the boundary between JSON API's normalizers and core's
  3. … sadly this also updates HttpExceptionNormalizer, because HttpExceptionNormalizerValue surprisingly is basically an alias for FieldNormalizerValue. Changing that is out-of-scope here, so making the minimum changes necessary.
wim leers’s picture

StatusFileSize
new11.59 KB
new29.46 KB

Now we can also finally delete one of the most dangerous bits of code, which was simultaneously the culprit for the problem being fixed here, and a consequence of HEAD's dealing with cacheability-in-value-objects-but-not-really-because-they're-mutable.

So, all of this disappears from \Drupal\jsonapi\Normalizer\EntityNormalizer:

-  protected function serializeField($field, array $context, $format) {
-    /* @var \Drupal\Core\Field\FieldItemListInterface|\Drupal\jsonapi\Normalizer\Relationship $field */
-    // Continue if the current user does not have access to view this field.
-    $access = $field->access('view', $context['account'], TRUE);
-    $context['cacheable_metadata']->addCacheableDependency($access);
-    if ($field instanceof AccessibleInterface && !$access->isAllowed()) {
-      return (new NullFieldNormalizerValue())->addCacheableDependency($access);
-    }
-    /** @var \Drupal\jsonapi\Normalizer\Value\FieldNormalizerValue $output */
-    $output = $this->serializer->normalize($field, $format, $context);
-    if (!$output instanceof FieldNormalizerValueInterface) {
-      return new NullFieldNormalizerValue();
-    }
-    $is_relationship = $this->isRelationship($field);
-    $property_type = $is_relationship ? 'relationships' : 'attributes';
-    $output->setPropertyType($property_type);
-
-    if ($output instanceof RefinableCacheableDependencyInterface) {
-      // Add the cache dependency to the field level object because we want to
-      // allow the field normalizers to add extra cacheability metadata.
-      $output->addCacheableDependency($access);
-    }
-
-    return $output;
-  }

and instead we move that logic into \Drupal\jsonapi\Normalizer\FieldNormalizer:

  public function normalize($field, $format = NULL, array $context = []) {
    /* @var \Drupal\Core\Field\FieldItemListInterface $field */

    $access = $field->access('view', $context['account'], TRUE);
    $property_type = static::isRelationship($field) ? 'relationships' : 'attributes';

    if ($access->isAllowed()) {
      $normalized_field_items = $this->normalizeFieldItems($field, $format, $context);
      assert(Inspector::assertAll(function ($v) {
        return $v instanceof FieldItemNormalizerValue;
      }, $normalized_field_items));

      $cardinality = $field->getFieldDefinition()
        ->getFieldStorageDefinition()
        ->getCardinality();
      return new FieldNormalizerValue($access, $normalized_field_items, $cardinality, $property_type);
    }
    else {
      return new NullFieldNormalizerValue($access, $property_type);
    }
  }

So then then normalizing of fields becomes self-contained!

wim leers’s picture

StatusFileSize
new5.59 KB
new29.28 KB

Now we ahve nearly identical cacheability merging logic in 3 places. We can make that logic shareable by introducing a trait.

wim leers’s picture

StatusFileSize
new5.19 KB
new33.68 KB

And finally, some sad work-around fixes due to the fact that:

  1. ConfigEntityNormalizer uses FieldNormalizerValue, even though config entities don't have fields
  2. HttpExceptionNormalizer because HttpExceptionNormalizerValue surprisingly is basically an alias for FieldNormalizerValue (as mentioned in #32.3)
  3. RelationshipItemNormalizer(Value) because they subclass FieldItemNormalizer(Value)

Fixing either of those more elegantly is definitely out of scope here: this is making the minimal changes necessary to allow those things to work as intended.

Status: Needs review » Needs work

The last submitted patch, 35: 2940342-35.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new2.17 KB
new33.54 KB

I was a bit too eager in #33, by simplifying EntityNormalizer, I also broke ConfigEntityNormalizer. Fixed.

Status: Needs review » Needs work

The last submitted patch, 37: 2940342-37.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new34.21 KB
new701 bytes

Looks like I might have fixed #2930217: getRelated() not working when related entity field is empty accidentally, because Drupal\Tests\jsonapi\Functional\UserTest::testGetIndividual() is now failing because the roles relationship is now showing up, which was not the case before.

Updating the assertion.

Status: Needs review » Needs work

The last submitted patch, 39: 2940342-39.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new10.24 KB
new40.15 KB

Most of the remaining failures:

Error: Call to undefined method Drupal\jsonapi\Normalizer\Value\RelationshipItemNormalizerValue::addCacheableDependency()

… because \Drupal\jsonapi\Normalizer\RelationshipItemNormalizer::normalize() first constructs a RelationshipItemNormalizerValue value object and then modifies it, to set the includes, if any. By simply shifting that around so that the constructor already receives the includes (if any), we can remove all that complexity:

-      $normalizer_value->addCacheableDependency($entity_and_access['access']);
-      $normalizer_value->addCacheableDependency($included_normalizer_value);
-      // Add the cacheable dependency of the included item directly to the
-      // response cacheable metadata. This is similar to the flatten include
-      // data structure, instead of a content graph.
-      if (!empty($context['cacheable_metadata'])) {
-        $context['cacheable_metadata']->addCacheableDependency($normalizer_value);
-      }

(Again: rather than modifying a value object after-the-fact to add cacheability for data that it already contains, shifting that responsibility to the value object itself prevents an entire class of problems! And in doing so, we even removed two occurrences of $context['cacheable_metadata'].)

Those first two lines are what triggered the error in the tests. I had hoped to not also remove ::setIncludes() in this issue, but this forced me to do so anyway. As you can see, it's all related: all those setters/mutable value objects cause problems wrt cacheability!

In doing so, I noticed that 100% of the logic for includes lives in Field(Item)NormalizerValue, but 0% of the time do those contain any includes. In fact, they can't! The only time an include is even possible, is in Relationship(Item)NormalizerValue, which is a subclass of Field(Item)NormalizerValue. So, moved the little remaining logic in there.

Status: Needs review » Needs work

The last submitted patch, 41: 2940342-41.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new1.1 KB
new40.19 KB

As of #41, there's quite a bit of this:

Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
-MISS
+UNCACHEABLE

It's related to:

1) Drupal\Tests\jsonapi\Kernel\Normalizer\JsonApiDocumentTopLevelNormalizerTest::testNormalize with data set #0 ('uid,field_tags,field_image')
Failed asserting that 0 is identical to -1.

/var/www/html/modules/contrib/jsonapi/tests/src/Kernel/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php:285

2) Drupal\Tests\jsonapi\Kernel\Normalizer\JsonApiDocumentTopLevelNormalizerTest::testNormalize with data set #1 ('uid, field_tags, field_image')
Failed asserting that 0 is identical to -1.

Both have the same root cause (using NULL as a cacheable dependency, which results in max-age=0 being set, which makes the whole thing uncacheable). Simple fix.

I love how the immutability/stricter architecture seemingly is already helping us.

Status: Needs review » Needs work

The last submitted patch, 43: 2940342-43.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new8.63 KB
new46.45 KB

So, UserTest was failing because we were inappropriately granting access to the roles relationship. (That also means #39 was wrong — it actually uncovered a bug!)

Since #33, it's no longer \Drupal\jsonapi\Normalizer\EntityNormalizer::serializeField() that checks field access for every field. It's now FieldNormalizer. But … for entity reference fields, FieldNormalizer is never called! Then EntityReferenceFieldNormalizer() gets called. And EntityReferenceFieldNormalizer::normalize() was not yet updated to check field access. Fixing that fixed the last failure in UserTest, and also allowed me to revert #39. Great.

What's less great is that there's no good way to pass the access result's cacheability for a case of allowed access to an entity reference field ("relationship") onto the value object that eventually gets built, because the actual normalization happens in yet another normalizer. (EntityReferenceFieldNormalizer generates a Relationship object that then is normalized.)

Note that this is also already broken in HEAD, because:

-    // Continue if the current user does not have access to view this field.
-    $access = $field->access('view', $context['account'], TRUE);
…
-    /** @var \Drupal\jsonapi\Normalizer\Value\FieldNormalizerValue $output */
-    $output = $this->serializer->normalize($field, $format, $context);
-    if (!$output instanceof FieldNormalizerValueInterface) {
-      return new NullFieldNormalizerValue();
-    }
…
-    if ($output instanceof RefinableCacheableDependencyInterface) {
-      // Add the cache dependency to the field level object because we want to
-      // allow the field normalizers to add extra cacheability metadata.
-      $output->addCacheableDependency($access);
-    } 

The ::normalize() call returned a Relationship value object, and that is not an instance of RefinableCacheableDependencyInterface.

But fortunately there also was:

-    $context['cacheable_metadata']->addCacheableDependency($access);

… which just ignored the self-containedness of the value objects and just bubbled the access result's cacheability straight to the response. So there was no bug in practice, even though the code did not make sense.

Attached patch fixes all that:

  1. updates EntityReferenceFieldNormalizer::normalize() to do field access checking, and work like FieldNormalizer::normalize() as much as possible
  2. updates Relationship to implement CacheableDependencyInterface and accept the field access result, so that the access result cacheability can be transported over to the RelationshipNormalizerValue value object.
  3. consequently, RelationshipNormalizerValue no longer has to hardcode an "allowed" access result in its call to the parent constructor
  4. reverts the expectation changes in UserTest made in #33

Status: Needs review » Needs work

The last submitted patch, 45: 2940342-45.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new7.14 KB
new46.45 KB

This fixes the two Relationship(Item)NormalizerValueTests.

I was forced to also remove the testing test coverage that used nested FieldItemNormalizerValues. That failed horribly, because it ended up looking at the protected PHP object properties and interpreted those as values. Since the value object now carries cacheability, cacheability metadata was being included in the rasterization…
Not only does it make little sense, we also have conclusive proof that this is not used anywhere: the integration tests would have caught the problem long ago. The only occurrence of this was in this unit test!

In RelationshipNormalizerValueTest, there was a need to add cacheability to the constructor, which led to me adding explicit unit test coverage for the value object's cacheability reflecting that of its inputs, yay!

Status: Needs review » Needs work

The last submitted patch, 47: 2940342-47.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new7.07 KB
new53.54 KB

And this fixes the remaining unit tests:

  1. EntityNormalizerValueTest needed cacheability in constructors, and ended up getting explicit test coverage for it too. Much like RelationshipNormalizerValueTest.
  2. Same for FieldNormalizerValueTest.

Status: Needs review » Needs work

The last submitted patch, 49: 2940342-49.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

StatusFileSize
new60.54 KB

I can't reproduce those failures locally :/

LOL! I've been rolling my patches wrong, sorry. The patch in #47 was identical to that in #45, the right patch was in #49. I've been creating patches without the last local commit… 😅 The interdiffs were correct though.

Here's what #49 should have been.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new4.79 KB
new62.26 KB

GREEN! Finally!

But it's still failling horribly on Drupal 8.4, because CacheableDependencyTrait is new in 8.5. So for now, let JSON API ship with its own equivalent.

Status: Needs review » Needs work

The last submitted patch, 52: 2940342-52.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new3.09 KB
new63.01 KB

Still fails on 8.4 for the same reasons the first patch iteration failed on 8.4. Quoting #19:

#17 still failed on 8.4 because #2626924: Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata only landed in 8.5. Fixing.

wim leers’s picture

StatusFileSize
new5.45 KB
new64.35 KB

Fixing CS violations.

wim leers’s picture

StatusFileSize
new11.97 KB
new76.15 KB

And finally, lifting #23 from the previous patch iteration:

Many of the JSON API integration tests include this:

[…]

wim leers’s picture

StatusFileSize
new922 bytes
new76.15 KB

Oops, #52 introduced two new CS violations.

wim leers’s picture

Issue tags: +blocker

I now see that I didn't yet explain why I started working on this issue: because it's a hard blocker for #2883086: [PP-1] Port RequestHandler + ResourceResponseSubscriber improvements from REST module to JSON API. This is a hard blocker because without the fixes in this issue, it's too easy to introduce regressions in #2883086

Note that #2883086: [PP-1] Port RequestHandler + ResourceResponseSubscriber improvements from REST module to JSON API in turn is a blocker for other issues.

wim leers’s picture

Assigned: wim leers » Unassigned

This issue is now ready for review.

gabesullice’s picture

Status: Needs review » Reviewed & tested by the community

Whoa! What an effort! I love this. This will hopefully make it much easier to reason about things and make changes.

  1. +++ b/src/Normalizer/ConfigEntityNormalizer.php
    @@ -46,12 +47,12 @@ class ConfigEntityNormalizer extends EntityNormalizer {
    -    $output = new FieldNormalizerValue(
    +    return new FieldNormalizerValue(
    ...
    +      'attributes'
    ...
    -    $output->setPropertyType('attributes');
    -    return $output;
    

    This is a simplification we can make because the property type is in the constructor.

  2. +++ b/src/Normalizer/ConfigEntityNormalizer.php
    @@ -46,12 +47,12 @@ class ConfigEntityNormalizer extends EntityNormalizer {
    +      AccessResult::allowed(),
    

    Can we add a comment that explains why this is automatically allowed?

  3. +++ b/src/Normalizer/EntityNormalizer.php
    @@ -99,34 +95,13 @@ class EntityNormalizer extends NormalizerBase implements DenormalizerInterface {
    +      $normalized_field = $this->serializeField($field, $context, $format);
    +      assert($normalized_field instanceof FieldNormalizerValueInterface);
    

    This adds safety in case someone is overriding our serializers and is "wrongly" returning an array.

  4. +++ b/src/Normalizer/EntityNormalizer.php
    @@ -99,34 +95,13 @@ class EntityNormalizer extends NormalizerBase implements DenormalizerInterface {
    -    $output = new EntityNormalizerValue($normalizer_values, $context, $entity, $link_context);
    -    // Add the entity level cacheability metadata.
    -    $output->addCacheableDependency($entity);
    -    $output->addCacheableDependency($output);
    -    // Add the field level cacheability metadata.
    -    array_walk($normalizer_values, function ($normalizer_value) {
    -      if ($normalizer_value instanceof RefinableCacheableDependencyInterface) {
    -        $normalizer_value->addCacheableDependency($normalizer_value);
    -      }
    -    });
    

    This can all be removed because it's happening in the constructor and using the CacheableDependencyTraits now :)

  5. +++ b/src/Normalizer/EntityNormalizer.php
    @@ -99,34 +95,13 @@ class EntityNormalizer extends NormalizerBase implements DenormalizerInterface {
    -  /**
    -   * Checks if the passed field is a relationship field.
    ...
    -  protected function isRelationship($field) {
    
    +++ b/src/Normalizer/FieldNormalizer.php
    @@ -32,7 +36,37 @@ class FieldNormalizer extends NormalizerBase {
    +  protected static function isRelationship($field) {
    

    This got moved to the field normalizer and made static.

  6. +++ b/src/Normalizer/EntityNormalizer.php
    @@ -213,29 +188,7 @@ class EntityNormalizer extends NormalizerBase implements DenormalizerInterface {
    -    $access = $field->access('view', $context['account'], TRUE);
    -    $context['cacheable_metadata']->addCacheableDependency($access);
    -    if ($field instanceof AccessibleInterface && !$access->isAllowed()) {
    -      return (new NullFieldNormalizerValue())->addCacheableDependency($access);
    -    }
    
    +++ b/src/Normalizer/FieldNormalizer.php
    @@ -32,7 +36,37 @@ class FieldNormalizer extends NormalizerBase {
    +    $access = $field->access('view', $context['account'], TRUE);
    +    $property_type = static::isRelationship($field) ? 'relationships' : 'attributes';
    ...
    +    if ($access->isAllowed()) {
    ...
    +      return new NullFieldNormalizerValue($access, $property_type);
    

    This checking got moved to the field normalizer and the access result is passed to the normalizer value.

  7. +++ b/src/Normalizer/EntityReferenceFieldNormalizer.php
    @@ -81,6 +82,13 @@ class EntityReferenceFieldNormalizer extends FieldNormalizer implements Denormal
       public function normalize($field, $format = NULL, array $context = []) {
    ...
    +    $field_access = $field->access('view', $context['account'], TRUE);
    +    if (!$field_access->isAllowed()) {
    +      return new NullFieldNormalizerValue($field_access, 'relationships');
    +    }
    

    Now field access gets checked "closer" to its normalization.

  8. +++ b/src/Normalizer/FieldItemNormalizer.php
    @@ -30,6 +31,11 @@ class FieldItemNormalizer extends NormalizerBase {
    +   * This normalizer exists JSON API normalizer land and enters the land of
    

    s/exists/leaves/

  9. +++ b/src/Normalizer/FieldItemNormalizer.php
    @@ -40,6 +46,10 @@ class FieldItemNormalizer extends NormalizerBase {
    +    $context['cacheability'] = new CacheableMetadata();
    
    @@ -47,7 +57,10 @@ class FieldItemNormalizer extends NormalizerBase {
    +    $value = new FieldItemNormalizerValue($values, $context['cacheability']);
    +    unset($context['cacheability']);
    

    This is how we "catch" core's cacheability information.

  10. +++ b/src/Normalizer/FieldNormalizer.php
    @@ -32,7 +36,37 @@ class FieldNormalizer extends NormalizerBase {
    +      $normalized_field_items = $this->normalizeFieldItems($field, $format, $context);
    +      assert(Inspector::assertAll(function ($v) {
    +        return $v instanceof FieldItemNormalizerValue;
    +      }, $normalized_field_items));
    

    Again, adding safety in case normalizers were overridden and are not following JSON API's Value pattern.

  11. +++ b/src/Normalizer/FieldNormalizer.php
    @@ -32,7 +36,37 @@ class FieldNormalizer extends NormalizerBase {
    +      $cardinality = $field->getFieldDefinition()
    +        ->getFieldStorageDefinition()
    +        ->getCardinality();
    
    @@ -62,10 +96,7 @@ class FieldNormalizer extends NormalizerBase {
    -    $cardinality = $field->getFieldDefinition()
    -      ->getFieldStorageDefinition()
    -      ->getCardinality();
    

    This just was moved.

  12. +++ b/src/Normalizer/HttpExceptionNormalizer.php
    @@ -49,12 +51,16 @@ class HttpExceptionNormalizer extends NormalizerBase {
    +      // @todo Either this should not use FieldItemNormalizerValue, or FieldItemNormalizerValue needs to be renamed to not be semantically coupled to "fields".
    

    +1

  13. +++ b/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php
    @@ -230,11 +230,7 @@ class JsonApiDocumentTopLevelNormalizer extends NormalizerBase implements Denorm
    -      $output->setIncludes($output->getAllIncludes());
    
    +++ b/src/Normalizer/Value/FieldNormalizerValue.php
    @@ -106,20 +116,6 @@ class FieldNormalizerValue implements FieldNormalizerValueInterface {
    -  public function setIncludes(array $includes) {
    -    $this->includes = $includes;
    -  }
    
    +++ b/src/Normalizer/Value/FieldNormalizerValueInterface.php
    @@ -28,26 +28,6 @@ interface FieldNormalizerValueInterface extends ValueExtractorInterface, Refinab
    -  public function setIncludes(array $includes);
    
    +++ b/src/Normalizer/Value/NullFieldNormalizerValue.php
    @@ -58,13 +67,6 @@ class NullFieldNormalizerValue implements FieldNormalizerValueInterface {
    -  public function setIncludes(array $includes) {
    -    // Do nothing.
    -  }
    

    The only place setIncludes was here. That indicates it ought to be removed. Though, we're lacking solid included coverage, so I'd like to see that land before this.

  14. +++ b/src/Normalizer/Relationship.php
    @@ -66,6 +70,9 @@ class Relationship implements AccessibleInterface {
    +   * @param \Drupal\Core\Access\AccessResultInterface $view_access
    +   *   The 'view' field access result. (This value object is only ever used for
    +   *   normalization, and hence only for 'view' access.
    

    Nice clarification.

  15. +++ b/src/Normalizer/RelationshipItemNormalizer.php
    @@ -77,17 +74,17 @@ class RelationshipItemNormalizer extends FieldItemNormalizer {
    +      $included_normalizer_value = NULL;
    

    Maybe this should be using NullFieldNormalizerValue too? Not sure.

  16. +++ b/src/Normalizer/RelationshipNormalizer.php
    @@ -90,7 +91,12 @@ class RelationshipNormalizer extends NormalizerBase {
    +    // If this is called, Relationship access is always allowed. The
    +    // cacheability of the access result is carried by the Relationship value
    +    // object. Therefore, we can safely construct an access result object here.
    ...
    +    return new RelationshipNormalizerValue($relationship_access, $normalizer_items, $cardinality, $link_context);
    

    This statement is true on the field level. But it's initially a little scary because I know that access to the relationship targets has not happened yet. Let's clarify the comment to say "If this is called, access to the Relationship field is allowed. Access to the targeted related resources will be checked separately."

  17. +++ b/src/Normalizer/Value/FieldNormalizerValue.php
    @@ -44,21 +46,29 @@ class FieldNormalizerValue implements FieldNormalizerValueInterface {
    +   * @param string $property_type
    ...
    +  public function __construct(AccessResultInterface $field_access_result, array $values, $cardinality, $property_type) {
    +    assert($property_type === 'attributes' || $property_type === 'relationships');
    
    @@ -106,20 +116,6 @@ class FieldNormalizerValue implements FieldNormalizerValueInterface {
    -  public function setPropertyType($property_type) {
    -    $this->propertyType = $property_type;
    -  }
    

    This is now in the constructor.

  18. +++ b/src/Normalizer/Value/FieldNormalizerValue.php
    @@ -44,21 +46,29 @@ class FieldNormalizerValue implements FieldNormalizerValueInterface {
         $this->includes = array_map(function ($value) {
    -      if (!$value instanceof FieldItemNormalizerValue) {
    -        return new NullFieldNormalizerValue();
    +      if (!$value instanceof RelationshipItemNormalizerValue) {
    +        return NULL;
           }
    

    This is because includes can only ever be relationships.

  19. +++ b/src/Normalizer/Value/RelationshipItemNormalizerValue.php
    @@ -22,16 +21,36 @@ class RelationshipItemNormalizerValue extends FieldItemNormalizerValue implement
    +   * Included normalized entity, if any.
    ...
    +   * @var \Drupal\jsonapi\Normalizer\Value\EntityNormalizerValue|null
    

    I think this could be an HttpExceptionNormalizerValue too, if access was denied to the related entity.

  20. +++ b/src/Normalizer/Value/RelationshipItemNormalizerValue.php
    @@ -22,16 +21,36 @@ class RelationshipItemNormalizerValue extends FieldItemNormalizerValue implement
    +   * @param \Drupal\jsonapi\Normalizer\Value\EntityNormalizerValue|null $include
    +   *   The included normalized entity, or NULL.
    

    Same.

  21. +++ b/tests/src/Kernel/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php
    @@ -136,6 +139,10 @@ class JsonApiDocumentTopLevelNormalizerTest extends JsonapiKernelTestBase {
    +        'format' => 'plain_text',
    

    We should add a @todo to test processed text in the tests for ensuring data type normalized values are the same across the ecosystem.

  22. +++ b/tests/src/Kernel/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php
    @@ -680,6 +687,68 @@ class JsonApiDocumentTopLevelNormalizerTest extends JsonapiKernelTestBase {
    +  /**
    +   * Decorates a request with sparse fieldsets and includes.
    +   */
    +  protected function decorateRequest(Request $request, array $fields = NULL, array $includes = NULL) {
    

    Good idea ;)

gabesullice’s picture

Status: Reviewed & tested by the community » Needs work

Whoops, it does need work. But just a little bit :)

wim leers’s picture

Title: Cacheability metadata on an entity fields' properties is lost » [PP-1] Cacheability metadata on an entity fields' properties is lost
Status: Needs work » Needs review
StatusFileSize
new4.87 KB
new76.93 KB

Thanks for reviewing this massive patch! I hope that the incremental updates plus comments describing the "why" for each was helpful.

  1. Correct.
  2. ✔️
  3. Correct.
  4. Correct.
  5. Correct.
  6. Correct.
  7. Correct.
  8. ✔️ (That should've been exits obviously! Went with your suggestion though :))
  9. Correct. And important to note: how we prevent it from persisting in the background, from one field item to the next. IOW: the unset() guarantees there can't be any side effects.
  10. Correct.
  11. Correct.
  12. :)
  13. That's exactly how I feel! Initially, there was nothing include-related in this issue/patch, but I was forced to deal with that here too. Because the previous setInclude() call was not dealing with cacheability correctly, and the whole point of this issue is that we now do deal with it correctly. So, postponing this issue on #2945093: Comprehensive JSON API integration test coverage phase 3: test JSON API-specific use cases: related/relationship routes, includes and sparse field sets.
  14. Thanks! I spent some time wondering how this was supposed to work, glad to see my comments already helping.
  15. I tried to change as little as possible. RelationshipItemNormalizerValue extends FieldItemNormalizerValue. The latter used to contain $this->include, and it was set to NULL by default. The old code in RelationshipNormalizer constructed a RelationshipItemNormalizerValue, then modified it. The new code is required to pass in an include immediately. To keep it behave the exact same way, I'm passing NULL. You're right that perhaps it should become NullFieldNormalizerValue. But there's definitely no need to carry cacheability: in theory it should associate url.query_args:include, but \Drupal\jsonapi\Normalizer\Value\JsonApiDocumentTopLevelNormalizerValue::__construct() is in the current design the one to add that. So, A) to keep things as much unchanged as possible, B) because there's no associated cacheability, the current solution of using NULL is fine/correct/preferable.
  16. ✔️
  17. Correct.
  18. Correct.
  19. ✔️
  20. ✔️
  21. Discussed in chat: we already have integration tests for it in \Drupal\Tests\jsonapi\Functional\CommentTest::getExpectedDocument() and \Drupal\Tests\jsonapi\Functional\TermTest::getExpectedDocument(). At which point Gabe said "Oh, nice, nevermind then."
  22. Hah :P Yes, this is your test coverage code!
wim leers’s picture

Status: Needs review » Postponed
gabesullice’s picture

All my concerns have been addressed. RTBC once unblocked.

wim leers’s picture

Title: [PP-1] Cacheability metadata on an entity fields' properties is lost » Cacheability metadata on an entity fields' properties is lost
Status: Postponed » Reviewed & tested by the community

The blocking patch was committed in #2945093-50: Comprehensive JSON API integration test coverage phase 3: test JSON API-specific use cases: related/relationship routes, includes and sparse field sets! Would be great to have @e0ipso's explicit approval.

(#2945093 was only blocking because it helps ensure that this issue does not introduce regressions. #62 was already green, but I queued re-tests against both 8.4 and 8.6 now that #2945093-50 happened. If it still comes back green, that would proves this doesn't cause any regressions!)

wim leers’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new6.96 KB
new80.14 KB

After re-testing, it was green only on 8.4 (where there are no fields in core that bubble cacheability). On 8.6, the re-testing is failing 😞 … but that means that #2945093-50: Comprehensive JSON API integration test coverage phase 3: test JSON API-specific use cases: related/relationship routes, includes and sparse field sets is catching potential regressions wrt cacheability in sparse fieldset handling! 😀🎉

This is great!

What exactly is failing? Well, if a normalized field has certain cacheability associated with it, but that field is omitted (because not listed in a sparse fieldset), then of course that cacheability should also be absent. But the current test infrastructure assumes that any normalization of the entity always has the same cacheability. That's of course no longer true when sparse fieldsets come into play!

So, all we need to do, is refine our test infrastructure a little bit, to allow us to indicate we want the cacheability limited to a particular sparse fieldset, if any.

We need to do that for TermTest, CommentTest and BlockContentTest.

Status: Needs review » Needs work

The last submitted patch, 66: 2940342-66.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new2.08 KB
new81.17 KB

PHP 7/my local env lets me get away with the above, but testbot wisely requires me to add the optional parameter to all method overrides.

wim leers’s picture

StatusFileSize
new992 bytes
new81.84 KB

#68 should be green — and then conclusively proves that JSON API's sparse fieldset handling A) handles cacheability correctly, B) this patch does not cause any regressions in that area!

We can however add a small extra bit of test coverage to be even more certain.

gabesullice’s picture

Status: Needs review » Needs work
+++ b/tests/src/Functional/ResourceTestBase.php
@@ -1612,6 +1612,10 @@ abstract class ResourceTestBase extends BrowserTestBase {
+        $this->assertSame($this->getExpectedCacheTags($field_set), $this->getExpectedCacheTags());
+        $this->assertSame($this->getExpectedCacheContexts($field_set), $this->getExpectedCacheContexts());

Should this actually be a runtime assertion using assert? Since it's not actually testing anything except the validity of the tests themselves?

BTW, this is friggin' awesome to see all this testing stuff doing its job!

wim leers’s picture

StatusFileSize
new1.75 KB
new82.07 KB

Fixing CS violations.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new1.1 KB
new82.06 KB

#70: I contemplated the same thing :) Done!

gabesullice’s picture

Status: Needs review » Reviewed & tested by the community

This is awesome :)

e0ipso’s picture

Status: Reviewed & tested by the community » Needs work

After reviewing this I have very good impressions. This refocuses some concepts that have been patched several times one on top of the other for a while now. Great work @Wim Leers!

I am a little confused with some scope creep regarding includes and relationships. I see that some of the functionality has been altered, and I'm interested to learn the reasons before merging this. Many of the things that got changed and comments added seem to assume the entity resource as the only endpoint. However the related and relationship endpoints also need to be handled by the same codebase.

I don't have any specific concerns, but a general feeling. Did you keep the related and relationship endpoints (with their different HTTP methods) in mind when doing the refactor?

Marking as Needs Work to get a bit more clarity on this.

wim leers’s picture

Status: Needs work » Needs review

After reviewing this I have very good impressions. This refocuses some concepts that have been patched several times one on top of the other for a while now. Great work @Wim Leers!

WHEW! So glad to see you say that :) ❤️

I am a little confused with some scope creep regarding includes and relationships. I see that some of the functionality has been altered, and I'm interested to learn the reasons before merging this. […]

I don't have any specific concerns, but a general feeling. Did you keep the related and relationship endpoints (with their different HTTP methods) in mind when doing the refactor?

Ok, let me walk you through that.

Yes, I kept all of JSON API's capabilities in mind. Quoting a bit from the IS:

  1. To truly prevent these sorts of problems from reappearing in the future, we should also completely remove JSON API's use of $context['cacheable_metadata'], and rely solely on value objects.

    Sadly, we can't really complete this refactor yet because we lack solid test coverage for e.g. includes (this is being worked on in #2945093: Comprehensive JSON API integration test coverage phase 3: test JSON API-specific use cases: related/relationship routes, includes and sparse field sets). For example \Drupal\jsonapi\Normalizer\RelationshipItemNormalizer::normalize() specifically deals with includes and still uses $context['cacheable_metadata'].

    So, removing $context['cacheable_metadata'] is definitely out of scope for this issue. Created #2948666: Remove JSON API's use of $context['cacheable_metadata'] for that. This issue only refactors those bits that we do have great test coverage for: normalizing of fields and field items.

For example #60.13 and #62.13 also explicitly discussed includes. They explicitly say we should wait for the patch #2945093-50: Comprehensive JSON API integration test coverage phase 3: test JSON API-specific use cases: related/relationship routes, includes and sparse field sets to land, because the first part of that issue (committed by now, hence this was RTBC'd) explicitly adds test coverage for includes!

Up until #39, there was barely any change wrt includes/relationships. Starting in #41, there was an important refactor in RelationshipItemNormalizerValue. I was forced to do that because class RelationshipItemNormalizerValue extends FieldItemNormalizerValue: FieldItemNormalizerValue needed to be modified before, to fix the root problem properly (see e.g. #25). But because the relationship value objects build on top of the field ones, I now was required to fix those too! Doing that does make sense though: those relationship value objects also can carry cacheability, and hence are prone to similar bugs. By also enforcing the intended architecture for them, we can help prevent similar bugs there in the future.

That in turn forced me to do further fixes in #45 and so on.

If you have the chance, starting at #25 and then reading every comment + interdiff towards the end should give you a complete understanding of all the changes!


Long story short: the patches until before #25 were using $context['cacheable_metadata'] to fix the problem. But this meant we were still not yet fixing the root cause of the problem; it still meant we were not using JSON API's intended architecture. That intended architecture is: each level (entity, field, field item) gets normalized not into an array, but into a value object. That value object then allows the JSON API module to rasterize its data into multiple places into the final JSON API document. But that value object also carries the cacheability of the normalized data!
Sadly, the reality was that the value object rarely carried the intended cacheability (or even any at all). In many places, we worked around this by using $context['cacheability'], i.e. out-of-band bubbling of cacheability. In the places where we did put cacheability on the value object, we did so after the fact, i.e. after creating the value object. And this too resulted in severe bugs. Having the value objects receive information once and only once (i.e. making them immutable) makes everything more predictable. Because it forces us to first build child value objects and then pass them into the parent. The parent then can automatically inherit the cacheability of its child value objects … and the need for us to remember to manually set that cacheability on the parent object disappears.
Once I started on this path, I was forced to see it through for all value objects/all normalizers. That's why the patch is so big.

(Hah, that wasn't very short. But it does paint the overall picture!)


Hope this helps clarify things. If it doesn't, then I'm sorry I couldn't explain it better :( I propose we then set up a meeting to walk you through it, either during this weekend, or on Monday?

e0ipso’s picture

Status: Needs review » Fixed

WHEW! So glad to see you say that :) ❤️

I'm sorry if you ever thought it was in any other way.


I was thinking more about the relationship and related endpoints. I am going to merge this after tagging a release for this week so we can have a full week of testing this if necessary before releasing it.

  • e0ipso committed 4a6f14f on 8.x-1.x authored by Wim Leers
    Issue #2940342 by Wim Leers, gabesullice, e0ipso: Cacheability metadata...
e0ipso’s picture

Tagged and pushed 1.13. I merged this one after that tag.

Thanks again for some epic work here.

wim leers’s picture

I was thinking more about the relationship and related endpoints. I am going to merge this after tagging a release for this week so we can have a full week of testing this if necessary before releasing it.

Sounds good!

Thanks again for some epic work here.

<3

Status: Fixed » Closed (fixed)

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