Problem/Motivation

This was spawned from #2626924: Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata, which we realized would benefit enormously from some missing infrastructure, which is the infrastructure this issue is trying to add. Similarly, other data is missing in our serializations/normalizations, with the same infrastructure needs: #2577923: MenuLinkContent entities pointing to nodes are not deployable: LinkItem should have a "target_uuid" computed property, #2825487: Fix normalization of File entities: file entities should expose the file URL as a computed property on the 'uri' base field, #2825812: ImageItem should have an "derivatives" computed property, to expose all image style URLs.

The approach we worked on looked like this:

To accomplish this:

  • We are adding 2 computed field properties to TextItemBase.
  • Then we also have to create 2 TextItemNormalizers, one for the Serialization module (default normalization, for the json and xml formats) and one for the HAL module (HAL normalization, for the hal_json format).
  • Then of course we need the tests for the normalizers.

Then in #2825812: ImageItem should have an "derivatives" computed property, to expose all image style URLs which has similar problem of exposing extra read-only information to normalization
We accomplish this by:

  • Creating two ImageItemNormalizers , one for Serialization and one for HAL.
  • Then of course we need the tests for the normalizers.
  • But in this case we don't add the computed field properties ImageItem

You can see how this gets complicated very quickly:

  1. new normalizers, multiplied by all different normalizations in core (and that doesn't include the contrib normalizations yet, such as https://www.drupal.org/project/jsonapi)
  2. API-exposing modules not relying on normalizers, such as https://www.drupal.org/project/graphql, would have to again duplicate all this logic
  3. finally, automatically generating documentation that describes the structure of the generated responses also becomes impossible to automate, because they'd have to hardcode the special cases of every normalizer … which we could do in theory for Drupal core's normalizers, but impossible to know all the contrib/custom normalizers!

In other words: lots and lots of custom code, pretty much duplicated, with slight differences, and it all needs to be kept in sync. 😨😭 In reality, it'll mean each format has different omissions, and each format will have inaccurate response documentation.

Proposed resolution

Add

  • computed fields to entities (e.g. image style URLs for Image entities)
  • computed properties to fields (e.g. processed for TextItem fields)

to those things that need to expose information that API consumers currently are missing.

That means they are described by Typed Data. Which means they can be automatically normalized (for json, xml, hal_json, api_json). They can also be automatically be exposed by GraphQL. And they can automatically be documented by OpenAPI!

A complication here is that computed properties today include for example the entity property on EntityReferenceItem fields. That loads/contains the full Entity object in PHP, as a convenience for PHP code. But it'd be nonsensical/undesirable to normalize each entity reference field's entity property — it'd be a BC break for all formats for starters, too much data, weird, and so on.

After various different directions explored in this issue, we landed on the approach of adding DataDefinitionInterface::isInternal() and DataDefinition::setInternal(), after discussing this with the Entity/Field/Typed Data API maintainers at DrupalCon Vienna 2017 (see #95).
isInternal() would then default to FALSE (meaning the data is not internal and therefore would be normalized), except when isComputed() === TRUE, then it'd default to TRUE (meaning the data is internal and therefore would not be normalized — this would mean the entity property on EntityReferenceItem would be omitted from the normalization).

This would then also match the existing behavior of all normalizers: computed properties are not normalized by default (because \Drupal\Core\TypedData\Plugin\DataType\Map::getIterator() calls \Drupal\Core\TypedData\Plugin\DataType\Map::getProperties($include_computed = FALSE)), thus avoiding a BC break.

In other words: it's adding a single boolean flag to Typed Data's metadata that allows normalizers (in core and contrib), custom API modules (GraphQL) and documentation (OpenAPI) to not need custom code, instead allowing them to inspect this simple flag.

Remaining tasks

  1. Sign-off: we got +1s for the general approach from fago (Typed Data API maintainer), amateescu (Field API maintainer), hchonov (Entity API maintainer) at DrupalCon Vienna
  2. Get to RTBC.

User interface changes

None.

API changes

  1. API addition: DataDefinitionInterface::isInternal()
  2. API addition: DataDefinition::setInternal($internal) (not on the interface, which is consistent with other flags, and follows Typed Data API maintainers' recommendations)
  3. API addition: ComplexDataPropertiesNormalizerTrait — a trait to share common logic that normalizers dealing with \Drupal\Core\TypedData\ComplexDataInterface objects will need to implement to correctly respect isInternal()

Data model changes

None. (Arguably, internal should be added to the config schema for field.storage.*.* — but computed isn't in there either, so this is again just being consistent. This was also recommended by Field + Typed Data API maintainers at an in-person meeting at DrupalCon Vienna.)

CommentFileSizeAuthor
#160 2871591-160.patch34.03 KBtedbow
#160 interdiff-157-160.txt3.93 KBtedbow
#157 2871591-157.patch33.92 KBtedbow
#157 interdiff-151-157.txt797 bytestedbow
#151 2871591-151.patch33.93 KBtedbow
#151 interdiff-148-151.txt2 KBtedbow
#148 2871591-148.patch33.83 KBtedbow
#148 interdiff-145-148.txt8.35 KBtedbow
#145 2871591-145.patch31.8 KBtedbow
#145 interdiff-142-145.txt2.04 KBtedbow
#142 2871591-142.patch31.86 KBtedbow
#142 interdiff-140-142.txt8.19 KBtedbow
#140 interdiff-136-140.txt4.86 KBtedbow
#140 2871591-140.patch32.67 KBtedbow
#138 2871591-138.patch32.13 KBtedbow
#138 interdiff-136-138.txt4.86 KBtedbow
#137 interdiff.txt4.84 KBamateescu
#136 2871591-136.patch33.97 KBtedbow
#136 interdiff-133-136.txt11.38 KBtedbow
#133 2871591-131.patch36.39 KBtedbow
#133 interdiff-128-131.txt1.98 KBtedbow
#128 interdiff-2871591-127-128.txt700 bytesdagmar
#128 2871591-128.patch36.62 KBdagmar
#125 2871591-125.patch36.44 KBtedbow
#125 interdiff-123-125.txt4.49 KBtedbow
#123 2871591-123.patch34.35 KBtedbow
#123 interdiff-118-123.txt4.16 KBtedbow
#2 2871591-2.patch40.09 KBtedbow
#2 diff-2626924-145-text_process.txt16.67 KBtedbow
#10 interdiff-2871591-10.txt7.44 KBtedbow
#10 2871591-10.patch39.83 KBtedbow
#12 interdiff-2871591-10-12.txt6.94 KBtedbow
#12 2871591-12.patch35.05 KBtedbow
#17 Screen Shot 2017-05-12 at 4.37.11 PM.png136.67 KBtedbow
#17 interdiff-2871591-12-17.txt9.8 KBtedbow
#17 2871591-17.patch36.33 KBtedbow
#18 interdiff-2871591-17-18.txt3.67 KBtedbow
#18 2871591-18.patch35.84 KBtedbow
#21 2871591-21.patch40.71 KBtedbow
#21 interdiff-2871591-18-21.txt6.79 KBtedbow
#25 interdiff-2871591-21-25.txt1.56 KBtedbow
#25 2871591-25.patch41.1 KBtedbow
#26 2871591-26.patch23.26 KBtedbow
#26 interdiff-2871591-25-26.txt29.91 KBtedbow
#30 interdiff-26-30.txt10.25 KBtedbow
#30 2871591-30.patch25.28 KBtedbow
#33 interdiff-30-33.txt1.48 KBtedbow
#33 2871591-33.patch25.95 KBtedbow
#35 interdiff-33-35.txt2.26 KBtedbow
#35 2871591-35.patch26.45 KBtedbow
#40 2871591-40.patch25.39 KBtedbow
#43 interdiff.txt878 bytesWim Leers
#44 2871591-43.patch27.38 KBWim Leers
#50 2871591-49.patch26.65 KBtedbow
#50 interdiff-2871591-44-49.txt520 bytestedbow
#53 interdiff-50-53.txt6.3 KBtedbow
#53 2871591-53.patch31.87 KBtedbow
#66 interdiff-53-66.txt9 KBtedbow
#66 2871591-66.patch37.31 KBtedbow
#70 interdiff-53-70.txt16.26 KBtedbow
#70 2871591-70.patch33.21 KBtedbow
#72 interdiff-70-72.txt11.57 KBtedbow
#72 2871591-72.patch31.76 KBtedbow
#74 2871591-74.patch33.48 KBtedbow
#74 interdiff-72-74.txt3.79 KBtedbow
#74 2871591-74_plus-2908674.patch34.85 KBtedbow
#78 interdiff-74-78.txt7.62 KBtedbow
#78 2871591-78.patch31.2 KBtedbow
#80 interdiff-78-80.txt2.83 KBtedbow
#80 2871591-80.patch30.21 KBtedbow
#82 interdiff-80-path.txt1.37 KBtedbow
#82 2871591-80-plus-2908674-2.patch31.58 KBtedbow
#86 interdiff-80-86.txt6.58 KBtedbow
#86 2871591-86.patch26.33 KBtedbow
#88 interdiff.txt1.11 KBWim Leers
#88 2871591-88.patch27.41 KBWim Leers
#88 2871591-88-plus-2908674-2.patch28.91 KBWim Leers
#90 interdiff.txt11.58 KBWim Leers
#90 2871591-90.patch26.74 KBWim Leers
#90 2871591-90-plus-2908674-2.patch28.23 KBWim Leers
#97 interdiff-90-97.txt20.47 KBtedbow
#97 2871591-97.patch25.78 KBtedbow
#99 2871591-99.patch28.88 KBtedbow
#99 interdiff-97-99.txt9.91 KBtedbow
#101 interdiff-99-101.txt1.6 KBtedbow
#101 2871591-101.patch28.87 KBtedbow
#104 interdiff-101-104.txt11.75 KBtedbow
#104 2871591-104.patch37.61 KBtedbow
#107 interdiff-104-106.txt1.73 KBtedbow
#107 2871591-106.patch37.42 KBtedbow
#111 interdiff-107-111.txt11.31 KBtedbow
#111 2871591-111.patch31.83 KBtedbow
#115 interdiff-111-114.txt1.71 KBtedbow
#115 2871591-115.patch32.24 KBtedbow
#117 allow_complexdata_in-2871591-117.patch32.01 KBborisson_
#117 interdiff-2871591.txt2.33 KBborisson_
#118 interdiff.txt1.09 KBWim Leers
#118 allow_complexdata_in-2871591-118.patch31.88 KBWim Leers
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

tedbow created an issue. See original summary.

tedbow’s picture

Status: Active » Needs review
FileSize
40.09 KB
16.67 KB

This patch tries this idea starting from #2626924-145: Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata

It should still working for exposing processed text but doesn't require TextItem specific normalizers.

I have included an interdiff to show what can be removed from #2626924: Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata but still get the normalization.

Presumably #2825812: ImageItem should have an "derivatives" computed property, to expose all image style URLs could also be modified to use this method without requiring ImageItem specific normalizers just adding a "images_styles" computed property.

tedbow’s picture

Title: Allow Field Types to specify computed properties that should be exported in serialization and other contexts. » Allow Field Types to specify computed properties that should be exported in normalization and other contexts.
Related issues: +#2825812: ImageItem should have an "derivatives" computed property, to expose all image style URLs, +#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

Title: Allow Field Types to specify computed properties that should be exported in normalization and other contexts. » Allow @FieldType plugins to specify computed properties that should be exported in normalization and other contexts.
Issue summary: View changes
Issue tags: +Entity Field API

Great write-up!

I added formatting and clarified some things. Please confirm that my changes are all correct, in other words please confirm that I did not misinterpret.

I personally think this approach makes a ton of sense. Especially after looking at the patch:

  1. +++ b/core/modules/hal/hal.services.yml
    @@ -8,12 +8,6 @@ services:
    -  serializer.normalizer.text_item.hal:
    
    +++ b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
    @@ -30,6 +33,8 @@ public function normalize($field_item, $format = NULL, array $context = []) {
    diff --git a/core/modules/hal/src/Normalizer/TextItemBaseNormalizer.php b/core/modules/hal/src/Normalizer/TextItemBaseNormalizer.php
    
    diff --git a/core/modules/hal/src/Normalizer/TextItemBaseNormalizer.php b/core/modules/hal/src/Normalizer/TextItemBaseNormalizer.php
    deleted file mode 100644
    
    +++ b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
    @@ -220,7 +220,7 @@ public function testSerialize() {
    diff --git a/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    
    diff --git a/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    deleted file mode 100644
    
    +++ /dev/null
    @@ -1,8 +0,0 @@
    -services:
    -  serializer.normalizer.text_item_base:
    

    Hurray!

  2. +++ b/core/modules/hal/tests/src/Kernel/NormalizeTest.php
    @@ -175,7 +175,7 @@ public function testNormalize() {
    -          'processed' => "<p>{$values['field_test_text']['value']}</p>",
    +          'process_result' => "<p>{$values['field_test_text']['value']}</p>",
    

    So this is the only difference in the original patch versus this one.

    And that's only because TextItemBase's processed property does not include cacheability metadata, so to not break BC, a new computed property had to be defined, and it has this slightly different name. Makes sense.

Status: Needs review » Needs work

The last submitted patch, 2: 2871591-2.patch, failed testing.

dawehner’s picture

Do

+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -1215,4 +1215,19 @@ protected function assertResourceNotAvailable(Url $url, array $request_options)
+  public static function nestedKsort(&$array) {

any reason for this to be public?

  1. +++ b/core/lib/Drupal/Core/Field/Annotation/FieldType.php
    @@ -91,4 +91,11 @@ class FieldType extends DataType {
    +   * @var array
    

    I guess its string[]?

  2. +++ b/core/modules/hal/hal.services.yml
    @@ -8,12 +8,6 @@ services:
           - { name: normalizer, priority: 10 }
    -  serializer.normalizer.text_item.hal:
    -    class: Drupal\hal\Normalizer\TextItemBaseNormalizer
    -    arguments: ['@renderer']
    -    tags:
    -      # The priority needs to higher than serializer.normalizer.field_item.hal.
    -      - { name: normalizer, priority: 20 }
       serializer.normalizer.field.hal:
         class: Drupal\hal\Normalizer\FieldNormalizer
    

    Nice!

  3. +++ b/core/modules/hal/tests/src/Kernel/NormalizeTest.php
    @@ -175,7 +175,7 @@ public function testNormalize() {
               'format' => $values['field_test_text']['format'],
    -          'processed' => "<p>{$values['field_test_text']['value']}</p>",
    +          'process_result' => "<p>{$values['field_test_text']['value']}</p>",
             ],
    
    +++ b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php
    @@ -201,7 +201,7 @@ protected function getExpectedNormalizedEntity() {
               'value' => 'The name "llama" was adopted by European settlers from native Peruvians.',
               'format' => 'plain_text',
    -          'processed' => '<p>The name &quot;llama&quot; was adopted by European settlers from native Peruvians.</p>' . "\n",
    +          'process_result' => '<p>The name &quot;llama&quot; was adopted by European settlers from native Peruvians.</p>' . "\n",
             ],
    ...
         ];
    

    Oh, isn't that an API change? We could support that by using a hashmap instead of a list in the annotation.

  4. +++ b/core/modules/serialization/src/Normalizer/ComputedPropertiesNormalizerTrait.php
    @@ -0,0 +1,45 @@
    +  protected function getComputedAttributes(FieldItemInterface $field_item, $format, array $context) {
    ...
    +      $attribute = $this->serializer->normalize($property, $format, $context);
    ...
    +      if ($attribute instanceof CacheableDependencyInterface && isset($context['cacheability'])) {
    +        $context['cacheability']->addCacheableDependency($attribute);
    +
    +      }
    

    It feels like in an ideal world we would wrap the serializer to do this for us.

Wim Leers’s picture

#6.3: note that this is a change relative to the patch in #2825812: ImageItem should have an "derivatives" computed property, to expose all image style URLs. It's not a change relative to HEAD! HEAD doesn't output processed at all. This is what's in HEAD:

      'comment_body' => [
        [
          'value' => 'The name "llama" was adopted by European settlers from native Peruvians.',
          'format' => 'plain_text',
        ],
      ],
dawehner’s picture

#6.3: note that this is a change relative to the patch in #2825812: ImageItem normalizer should expose all public image style URLs. It's not a change relative to HEAD! HEAD doesn't output processed at all. This is what's in HEAD:

Ah good to know! Sorry for the confusion.

In general +1 for naming the field exactly like the computed property. This is IMHO much better.

amateescu’s picture

If the goal is to provide a way to document which computed properties are "special" for the Schemata module (which says it works with Typed Data), shouldn't this new annotation property be at the typed data level?

+++ b/core/modules/text/src/TextProcessedResult.php
@@ -0,0 +1,92 @@
+class TextProcessedResult extends TypedData {

Instead of over-complicating things with this new class, why don't we send some 'return_as_object' setting to the existing class and do the FilterProcessResult dance based on that?

tedbow’s picture

Status: Needs work » Needs review
FileSize
7.44 KB
39.83 KB

UPDATE: I changed my mind since this first part but wanted to leave in my thinking incase others think this makes more sense
@amateescu,

If the goal is to provide a way to document which computed properties are "special" for the Schemata module (which says it works with Typed Data), shouldn't this new annotation property be at the typed data level?

The behaviour for the schemata module would be a benefit but is not the reason for the proposed change.

The issues mentioned in the summary:
#2626924: Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata
#2825812: ImageItem should have an "derivatives" computed property, to expose all image style URLs

Are about exposing related information in the normalization process. This idea is to simplify the process but also make sure the related information is not lost to the type data aware system like Schemata but not only schemata.

I think the reason to do this on the field level and not the TypeData level is because the Field level is where the normalization level has been created to not expose all computed properties. And it makes sense not to expose all computed properties on expose all TypeData properties on fields.

Would there be other uses of TypedData properties outside of the field system where the default would be to not expose all computed properties?
I don't think so. But could be wrong.

I am not familiar with TypedData uses outside of the field system. One place I do see it being use is \Drupal\Core\Field\FieldConfigBase::getItemDefinition it doesn't a computed property as far as I can tell.

AFTER THINKING ON IT, CHANGED MY MIND
Ok after trying to see how these might be done on the TypedData level I think it could be good idea and doable
Or at the very least something to explore and to get feedback from those who know TypedData better than me.

This patch:

  1. We can do this by adding a new property, $export_computed_properties, to \Drupal\Core\Field\Annotation\FieldType.

    Remove this.

  2. Adds \Drupal\Core\TypedData\ExposableDataDefinitionInterface
  3. Changes \Drupal\Core\TypedData\DataDefinition to implement ExposableDataDefinitionInterface
  4. Instead of using the annotation field types would use \Drupal\Core\TypedData\ExposableDataDefinitionInterface::setExposed() to specify properties that should be exposed.
  5. Updates \Drupal\serialization\Normalizer\ComputedPropertiesNormalizerTrait::getComputedAttributes() to look for properties that both computed and exposed.

Again the TextItem changes are still here to demonstrate that this is feasible. They should be removed it we decide this is whole issue is good idea.

For example of how to test a property as exposed see \Drupal\text\Plugin\Field\FieldType\TextItemBase::propertyDefinitions

$properties['process_result'] = DataDefinition::create('string')
      ->setLabel(t('Processed text (object)'))
      ->setDescription(t('The text with the text format applied.'))
      ->setComputed(TRUE)
      ->setClass(TextProcessedResult::class)
      ->setSetting('text source', 'value')
      ->setExposed(TRUE);

I am not sure about the backwards compatibility implications of 1 and 2 in the list above.

I think functionality it will be the same as using the annotations. I personally like this way better.
Thanks for the suggestion @amateescu!

Status: Needs review » Needs work

The last submitted patch, 10: 2871591-10.patch, failed testing.

tedbow’s picture

Status: Needs work » Needs review
FileSize
6.94 KB
35.05 KB

Ok errors make sense

1) Drupal\Tests\serialization\Unit\Normalizer\EntityReferenceFieldItemNormalizerTest::testNormalize
Prophecy\Exception\Call\UnexpectedCallException: Method call:
- getProperties(true)
on Double\Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem\P11 was not expected, expected calls were:
- getIterator()
- get(exact("entity"))

\Drupal\serialization\Normalizer\ComputedPropertiesNormalizerTrait::getComputedAttributes() now calls \Drupal\Core\TypedData\ComplexDataInterface::getProperties(TRUE) to get all properties including computed ones to determine which ones are "exposed".

This caused 2 error in this same test.

Text.Drupal\Tests\text\Kernel\Normalizer\TextItemBaseNormalizerTest errors.
This test should have been removed because the covered class \Drupal\text\Normalizer\TextItemBaseNormalizer no longer exists.

There is no need to have normalizer for TextItems any more because this issue handles this in a more generic way.

dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/TypedData/ExposableDataDefinitionInterface.php
    @@ -0,0 +1,31 @@
    +/**
    + * Interface for defining exposed data values.
    + *
    + * Should only be set for computed values.
    + */
    +interface ExposableDataDefinitionInterface {
    

    I assume we won't commit this to 8.3.x. Given that I'm wondering whether we really need the new interface ... we can add methods to interface, especially when there is a clear base class (DataDefinition).

  2. +++ b/core/lib/Drupal/Core/TypedData/ExposableDataDefinitionInterface.php
    @@ -0,0 +1,31 @@
    +  /**
    +   * Sets the whether the data value should be exposed.
    +   *
    +   * @param bool $exposed
    +   *   Whether the data value is exposed.
    +   *
    +   * @return static
    +   *   The object itself for chaining.
    +   */
    +  public function setExposed($exposed);
    ...
    +   * Determines whether the data value is exposed.
    +   *
    +   * @return bool
    +   *   Whether the data value is exposed.
    +   */
    +  public function isExposed();
    

    IMHO we should explain what exposing means ... aka. it will be returned in the normalization process / REST

  3. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestTextItemNormalizerTest.php
    @@ -0,0 +1,81 @@
    +      'value' => 'Cádiz is the oldest continuously inhabited city in Spain and a nice place to spend a Sunday with friends.',
    

    I like some knowledge transported via tests.

Wim Leers’s picture

#9: That's a very interesting point! Typed Data is the Single Source of Truth, that's how REST/JSON API/GraphQL can work, that's how Schemata/OpenAPI can work. And that's indeed the point of this issue: to not put extra logic in normalizers, but instead provide additional metadata. I think you're right in pointing out that this metadata does not belong in the @FieldType plugin annotation. Because in the end, a @FieldType is just a unit of abstraction, a unit of packaging. A unit/level of Typed Data. So why then put this metadata in that unit, rather than in the general system, which is the Typed Data API?
Great insight, thank you! :) In hindsight, it seems so obvious :D

(However, why don't we send some 'return_as_object' setting to the existing class and do the FilterProcessResult dance based on that? makes little sense to me, but maybe I'm misunderstanding this.)


#10 + #11:

  1. +++ b/core/lib/Drupal/Core/TypedData/ExposableDataDefinitionInterface.php
    @@ -0,0 +1,31 @@
    + * Interface for defining exposed data values.
    ...
    +interface ExposableDataDefinitionInterface {
    

    Is "exposed" the right term?

    I wonder if we want "normalizable", "serializable" or "exportable". Because this is really about exporting, normalizing, serializing Typed Data-based data structured for use in contexts other than PHP.

    REST is one example. But transporting this data via https://en.wikipedia.org/wiki/Protocol_Buffers for example would be another use case.

  2. +++ b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
    @@ -10,6 +11,8 @@
    +  use ComputedPropertiesNormalizerTrait;
    
    +++ b/core/modules/serialization/src/Normalizer/ComputedPropertiesNormalizerTrait.php
    @@ -0,0 +1,52 @@
    +/**
    + * Normalization methods for computed field properties.
    + */
    +trait ComputedPropertiesNormalizerTrait {
    
    +++ b/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php
    @@ -11,6 +11,8 @@
    +  use ComputedPropertiesNormalizerTrait;
    
    @@ -54,4 +56,18 @@ protected function constructValue($data, $context) {
    +    $attributes = array_merge($attributes, $this->getComputedAttributes($object, $format, $context));
    

    Isn't this a misnomer at this point? It's no longer about computed properties alone…

  3. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
    @@ -411,9 +411,9 @@ public function testGet() {
    +    $this->nestedKsort($expected);
    ...
    +    $this->nestedKsort($actual);
    

    These could be simplified to static::nestedKsort()

  4. +++ b/core/modules/serialization/src/Normalizer/ComputedPropertiesNormalizerTrait.php
    @@ -0,0 +1,52 @@
    +      if ($data_definition->isComputed() && $data_definition instanceof ExposableDataDefinitionInterface) {
    +        if ($data_definition->isExposed()) {
    

    Rather than explicitly checking if computed fields/properties implement this interface and then also return TRUE, shouldn't we just be doing this for all fields/properties automatically?

    And shouldn't we ensure that all non-computed fields default to exposed === TRUE?

    Then there's no API change. There's just a need for computed fields/properties to explicitly opt in.

    So then:

    1. we iterate over all fields/properties automatically
    2. every non-computed field is opt-out (i.e. exposed by default)
    3. every computed field is opt-in (i.e. not exposed by default)

    That seems like a simple, elegant, non-BC breaking approach?

    (Of course, I may very well be forgetting about some things! In which case, I'd love to hear about that in the comments :))

Wim Leers’s picture

#13

  1. I like that. We've done similar things lately, for similar situations/cases. (I can't find links at the moment.)
  2. On the one hand, I agree, on the other, I disagree. It should not be tied to REST specifically. But it wouldn't heard to explicitly list that as an example use case. Also: this is why I think we may need a better name, see #14.1.
  3. :)
andypost’s picture

related issue shows that some complex properties does not fits in annotation

tedbow’s picture

Ok here is a patch that addresses some of the feedback
Re: #13

I assume we won't commit this to 8.3.x. Given that I'm wondering whether we really need the new interface ... we can add methods to interface, especially when there is a clear base class (DataDefinition).

UPDATE: I was wrong here see #21
After looking we don't have such a clear base class.

Interfaces that extend this interface
\Drupal\Core\TypedData\DataReferenceDefinitionInterface
\Drupal\Core\TypedData\ListDataDefinitionInterface
\Drupal\Core\TypedData\ComplexDataDefinitionInterface

I don't like the idea of introducing the new interface but just want to be clear it is will be more than just updating 1 base class.
If this change to DataDefinitionInterface would break 8 base classes in core is it going to be a headache in contrib and custom code?

#13.1 and #14.2
Yes exposed is probably not the right term. but this patch doesn't fix that.

#14.3
This part of the patch probably needs to be removed because it was need explicitly for TextItem processing.
@todo Remove all portions that were only needed for #2626924: Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata
@todo Write tests that prove "exposed"(or whatever we end up calling them) properties are available in normalized output.

#14.4

And shouldn't we ensure that all non-computed fields default to exposed === TRUE?

Yes updated \Drupal\Core\TypedData\DataDefinition::isExposed() to return TRUE if it is not computed and 'exposed' has not been set.

Ok, if we really want this to be not specific to the normalized process then the normalizer should not be responsible for determining which properties are exposed.

This patch overrides getIterator() in FieldItemBase so that a loop like:

foreach ($field_item as $name => $property) {
...
}

Will always loop through exposed properties.
This simplifies the the normalization process.
Then:
Removed ComputedPropertiesNormalizerTrait
Add FieldItemPropertiesNormalizerTrait which can be used by both FieldItemNormalizers from json and hal_json normalizers.

UPDATE:Ok looking how this would affect JSON API. Since \Drupal\jsonapi\Normalizer\FieldItemNormalizer::normalize is also using

foreach ($field_item as $name => $property) {
...
}

Then they would automatically be getting these new "exposed" fields. From my discussions with e0ipso, I don't think this is inline with that modules assumptions at least the exposed properties in the related issues.

So will update this patch anyways but I don't think overriding getIterator() in FieldItemBase is the right approach.

So probably this would be up to FieldItemPropertiesNormalizerTrait loop over the properties.
Another idea would be to add getExposedProperties() to FieldItemInterface

tedbow’s picture

This patch removes overriding getIterator() in FieldItemBase.

Instead it adds \Drupal\serialization\Normalizer\FieldItemPropertiesNormalizerTrait::getExposedProperties.

Again if we did add isExposed() to DataDefinitionInterface this would be much simpler. But as I noted #17 it is not just as simple as updating 1 base class.

The last submitted patch, 17: 2871591-17.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 18: 2871591-18.patch, failed testing.

tedbow’s picture

Status: Needs work » Needs review
FileSize
40.71 KB
6.79 KB

Test failure in #18 makes sense.

In EntityReferenceFieldItemNormalizerTest

$this->fieldItem->getProperties(TRUE)
      ->willReturn([])
      ->shouldBeCalled();

\Drupal\serialization\Normalizer\FieldItemPropertiesNormalizerTrait::getExposedProperties would rely on this returning all current none computed properties.

New patch which does:
1) If we want to introduce the concept of "exposed" properties in TypeData it should not be up the normalization system to determine which properties are exposed.

This patch adds \Drupal\Core\TypedData\ComplexDataInterface::getExposedProperties()

Since the doc says

Interface for complex data; i.e. data containing named and typed properties.

Everything that implements is going to need to now have the concept of "some of the my contain properties can be exposed"

2) Removes ExposableDataDefinitionInterface
I was wrong in #17

If this change to DataDefinitionInterface would break 8 base classes in core is it going to be a headache in contrib and custom code?

So adding isExposed and setExposed() to DataDefinitionInterface
Then creating ExposableDataDefinitionTrait to implement these methods.

Then ExposableDataDefinitionTrait just has to be used in:

  • \Drupal\Core\Field\FieldConfigBase
  • \Drupal\Core\TypedData\DataDefinition

Removing ExposableDataDefinitionInterface makes \Drupal\Core\TypedData\ComplexDataWithExposedPropertiesTrait::getExposedProperties() introduced above much simpler
Unit test will probably fail because of expected calls.

Status: Needs review » Needs work

The last submitted patch, 21: 2871591-21.patch, failed testing.

dawehner’s picture

This patch adds \Drupal\Core\TypedData\ComplexDataInterface::getExposedProperties()

I am wondering whether this should actually be the behaviour if you iterate over the entries in a complex data?
We could then even drop the method again, but I'm just thinking out loud here.

Wim Leers’s picture

This sounds promising! I'll hold off on reviewing until the patch is green, unless you want me to review before then.

tedbow’s picture

Title: Allow @FieldType plugins to specify computed properties that should be exported in normalization and other contexts. » Allow ComplexData in TypedData to specify computed properties that should be exported in normalization and other contexts.
Status: Needs work » Needs review
FileSize
1.56 KB
41.1 KB

Ok this hopefully get the patch to green!(fixes EntityReferenceFieldItemNormalizerTest locally for me)

Also updating the title to reflect that this is no longer FieldType plugin specific.

Re #23

I am wondering whether this(meaning ComplexDataInterface::getExposedProperties()) should actually be the behaviour if you iterate over the entries in a complex data?
We could then even drop the method again, but I'm just thinking out loud here.

Meaning drop getExposedProperties() and just override getIterator() in base classes that implement ComplexDataInterface?

I think a problem would be that would be that we are using "exposed" to on computed properties that should be available when somehow exporting TypedData.

Presumably we are going to be using this process in the future for computed properties that should be "exposed" but where computing the fields could actually very expensive. So it seems making this the default behavior in getIterator() would be expensive

tedbow’s picture

The reason this interdiff is so huge is that this patch removes all change that were only needed for #2626924: Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata

Those changes were only left in to prove exposed properties worked while I was working on this patch.

This patch also adds \Drupal\Tests\rest\Functional\EntityResource\EntityTest\EntityTestExposedPropertyNormalizerTest
Test the "exposed" properties normalization through the REST process.

There will need to be other tests.

I will update #2626924: Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata with a patch that only has what is needed for "processed_result" property when using this patch.

Wim Leers’s picture

Component: serialization.module » typed data system
Issue tags: +API-First Initiative

#25:

Presumably we are going to be using this process in the future for computed properties that should be "exposed" but where computing the fields could actually very expensive. So it seems making this the default behavior in getIterator() would be expensive

This makes sense I think.

#26: Yay, a clean, focused patch!

There will need to be other tests.

For which aspects is test coverage missing then?


Patch review:

  1. +++ b/core/lib/Drupal/Core/TypedData/ComplexDataInterface.php
    @@ -102,4 +102,12 @@ public function toArray();
    +   * Gets exposed properties for the field item.
    

    This interface is not field-specific, so should not mention it.

    Look at the \Drupal\Core\TypedData\ComplexDataInterface::getProperties() docs for inspiration.

  2. +++ b/core/lib/Drupal/Core/TypedData/DataDefinitionInterface.php
    @@ -218,4 +218,23 @@ public function getConstraint($constraint_name);
    +  public function setExposed($exposed);
    

    Interestingly, DataDefinitionInterface does not contain setRequired() etc, in fact: this is the only setter, besides addConstraints().

    Yet \Drupal\Core\TypedData\DataDefinition does have a setRequired() method. So it doesn't live on the interface.

    I wonder why?

    Does that mean this method shouldn't be added to the interface either?

    (Perhaps the intent is that only getters are on the interface, no setters?)

  3. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestExposedPropertyNormalizerTest.php
    @@ -0,0 +1,87 @@
    +    // If the exposed test field has not been created create it.
    

    Nit: You can leave this comment out; we don't have it at \Drupal\Tests\rest\Functional\EntityResource\Node\NodeResourceTestBase::createEntity() either

  4. +++ b/core/modules/serialization/src/Normalizer/FieldItemPropertiesNormalizerTrait.php
    @@ -0,0 +1,42 @@
    + * Normalization methods for computed field properties.
    

    Not just for computed field properties.

I think this really needs a review from a Typed Data maintainer (of which there's only one: fago), or at least an Entity/Field API maintainer.

Wim Leers’s picture

Wim Leers’s picture

Status: Needs review » Needs work
+++ b/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php
@@ -150,14 +158,25 @@ protected function renderResponseBody(Request $request, ResourceResponseInterfac
+        'cacheability' => new CacheableMetadata(),

+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestExposedPropertyNormalizerTest.php
@@ -0,0 +1,87 @@
+class EntityTestExposedPropertyNormalizerTest extends EntityTestResourceTestBase {

+++ b/core/modules/system/tests/modules/entity_test/src/Plugin/DataType/ExposedStringData.php
@@ -0,0 +1,30 @@
+    return "Exposed! " . $string_value->getString();

Discussed with @tedbow.

The test coverage that's missing, is the cacheability metadata that these computed properties may have.

I think we can do that quite easily here: add a class similar to class FilterProcessResult extends BubbleableMetadata, which then carries this computed string plus its cacheability metadata (let's say it varies by the user cache context).

We can then test for that in the test, by overriding \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::getExpectedCacheContexts().

tedbow’s picture

Status: Needs work » Needs review
FileSize
10.25 KB
25.28 KB

@Wim Leers thanks for review!
#27
1. fixed
2. Yes I wondered this too. Didn't know if this was an oversight or intentional.
3. removed
4. Good point. So this also means that the whole trait should be changed from FieldItemPropertiesNormalizerTrait to ComplexDataPropertiesNormalizerTrait.

Further for the serialization module instead of invoking this in FieldItemNormalizer::normalize() this should be called in \Drupal\serialization\Normalizer\ComplexDataNormalizer::normalize() and FieldItemNormalizer no longer needs to override normalize().

For the HAL module there is no ComplexDataNormalizer so no change needed there.

This change doesn't break \Drupal\Tests\rest\Functional\EntityResource\EntityTest\EntityTestExposedPropertyNormalizerTest but it does break \Drupal\Tests\serialization\Unit\Normalizer\ComplexDataNormalizerTest but I think because the test is written.
The including \Drupal\Tests\serialization\Unit\Normalizer\TestComplexData doesn't implement the interface correctly. i.e. \Drupal\Tests\serialization\Unit\Normalizer\TestComplexData::getProperties returns no value so that breaks the trait.

Haven't fixed this test yet but lets see if it breaks anything else.

#28
Ok now testing this.

ExposedStringData is now returning the new \Drupal\entity_test\ComputedString which extends BubbleableMetadata.
Providing cache context and tags.

Status: Needs review » Needs work

The last submitted patch, 30: 2871591-30.patch, failed testing.

tedbow’s picture

#30

Haven't fixed this test yet but let's see if it breaks anything else.

Well this is an interesting failure

Drupal\Tests\serialization\Unit\Normalizer\EntityNormalizerTest::testNormalize
Argument 1 passed to Drupal\serialization\Normalizer\ComplexDataNormalizer::normalizeProperties() must be an instance of Drupal\Core\TypedData\ComplexDataInterface, instance of Mock_ContentEntityBase_a58ad0c0 given, called in /var/www/html/core/modules/serialization/src/Normalizer/ComplexDataNormalizer.php on line 30 and defined

Not what I expected.
So EntityNormalizerTest is causing a failure because it is calling the new ComplexDataPropertiesNormalizerTrait::normalizeProperties() but it is sending an object that doesn't implement ComplexDataInterface.

This is because what I did in point 4 in #30(from review in #27).

Further for the serialization module instead of invoking this in FieldItemNormalizer::normalize() this should be called in \Drupal\serialization\Normalizer\ComplexDataNormalizer::normalize() and FieldItemNormalizer no longer needs to override normalize().

So I was thinking that since the logic in ComplexDataPropertiesNormalizerTrait::normalizeProperties() worked with anything that implemented ComplexDataInterface I could just call this in ComplexDataNormalizer::normalize() because in ComplexDataNormalizer there is
protected $supportedInterfaceOrClass = 'Drupal\Core\TypedData\ComplexDataInterface';
So I thought that in ComplexDataNormalizer::normalize()

public function normalize($object, $format = NULL, array $context = []) {
  /** @var \Drupal\Core\TypedData\ComplexDataInterface $object */
  return $this->normalizeProperties($object, $format, $context);
}

$object would always implement ComplexDataInterface

Because \Drupal\serialization\Normalizer\FieldItemNormalizer extends \Drupal\serialization\Normalizer\ComplexDataNormalizer

FieldItemNormalizer has
protected $supportedInterfaceOrClass = FieldItemInterface::class;

And FieldItemInterface extends ComplexDataInterface

So I made the assumption that any normalizer that extended ComplexDataNormalizer would have a $supportedInterfaceOrClass that extended( or implemented) ComplexDataInterface

This is actually not the case and this causes the failure.

EntityNormalizer extends ComplexDataNormalizer

\EntityNormalizer::$supportedInterfaceOrClass = [EntityInterface::class];

EntityInterface does NOT implement ComplexDataInterface

So in EntityNormalizerTest::testNormalize() line 81 fails
$this->entityNormalizer->normalize($content_entity, 'test_format');

$content_entity does not implement ComplexDataInterface

EntityNormalizer doesn't override ComplexDataNormalizer::normalize()
So this causes a failure.

Whew! Is this confusing to anybody else?

Question 1: Given that why aren't all the Entity REST tests failing?

This is because none of the Entity REST tests are actually using EntityNormalizer directly. All the REST entity tests deal with entities that implement either ContentEntityInterface or ConfigEntityInterface.

So the entities are normalized with either ContentEntityNormalizer or ConfigEntityNormalizer
Both of these normalizers have overridden normalize() so ComplexDataNormalizer::normalize() is never called.

Question 2: Why was EntityNormalizer working before this patch?

This is because ComplexDataNormalizer::normalize() did this:

foreach ($object as $name => $field) {
   $attributes[$name] = $this->serializer->normalize($field, $format, $context);
}

So $object just had to implement Traversable

Moral of the story

Basically because the way we are extending normalizer classes for any normalizer in the Serializer module we cannot guarantee that $object sent its implementation of \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize() will implement the interface specified in its $supportedInterfaceOrClass
Even though doc for $supportedInterfaceOrClass says

The interface or class that this Normalizer supports.

Weird.

tedbow’s picture

Status: Needs work » Needs review
FileSize
1.48 KB
25.95 KB

So how should we fix the problem described in #32?

Option 1: Roll back changes to ComplexDataNormalizer::normalize()

This doesn't seem right because since amateescu pointed out #9 these exposed properties are more of TypedData thing. It has seemed to make more sense.

But moving the logic back to FieldItemNormalizer would work.

Option 2: Leave changes to ComplexDataNormalizer::normalize() and override normalize in EntityNormalizer

This would also make the tests pass because EntityNormalizer would be like ContentEntityNormalizer and not call ComplexDataNormalizer::normalize() with an $object that doesn't implement ComplexDataInterface

But if there were any contrib or custom normalizers that were like EntityNormalizer in that they extended ComplexDataNormalizer, did not override normalize() and their $supportedInterfaceOrClass did not implement ComplexDataInterface they would immediately break.

This seems like a BC break.

Option 3: In ComplexDataNormalizer::normalize() check if $object implements ComplexDataInterface

I am not huge fan of this but it seems like the only option that

  1. Doesn't break contrib normalizers that extend ComplexDataNormalizer
  2. Allows handling the concept "exposed" properties for other classes that implement ComplexDataInterface besides FieldItemInterface.

In addition we could use
We could use \Drupal\Core\Entity\Plugin\DataType\EntityAdapter::createFromEntity to create a TypedData object from an Entity when sent an Entity.
But since

  1. The object from createFromEntity will never have "exposed" and "non-exposed" properties and
  2. We still have to handle if some contrib normalizer is not sending EntityInterface or ComplexDataInterface object since the pattern we are demonstrating in core is $object has no relation to $supportedInterfaceOrClass

I am going to try option 3 in this patch.

Other ideas?

UPDATE:
chatted with @larowlan on IRC and he helped with extra info about EntityAdapter::createFromEntity and pointed out the Entities use to implement ComplexDataInterface early in the Drupal 8 development cycle(didn't know that). So that is probably why the normalizers are setup this way. Super helpful!

larowlan’s picture

Status: Needs review » Needs work
+++ b/core/lib/Drupal/Core/TypedData/DataDefinitionInterface.php
new file mode 100644
index 0000000000..708e40f3a9

index 0000000000..708e40f3a9
--- /dev/null

+++ b/core/modules/serialization/src/Normalizer/ComplexDataPropertiesNormalizerTrait.php
@@ -0,0 +1,41 @@
+  protected function normalizeProperties(ComplexDataInterface $data, $format, array $context) {
...
+    foreach ($data->getExposedProperties() as $name => $property) {

Cause you're only calling ->getExposedProperties here, I think your solution may be to add a new interface, then move getExposedProperties from ComplexDataInterface to the new interface. Then have both ComplexDataInterface and EntityInterface extend it. Then the implementation in EntityInterface can do something different - but you're typehinting for the method

tedbow’s picture

Status: Needs work » Needs review
FileSize
2.26 KB
26.45 KB

@larowlan that solves the problem only for EntityInterface but doesn't solve

We still have to handle if some contrib normalizer is not sending EntityInterface or ComplexDataInterface object since the pattern we are demonstrating in core is $object has no relation to $supportedInterfaceOrClass

So any contrib or custom normalizer that followed the pattern that core provided, i.e. extending ComplexDataNormalizer for a $supportedInterfaceOrClass that does not extend will have their normalizer immediately break.
I am sure this something we offer BC for but that feels like bad DX.

Also adding getExposedProperties to when we don't actually need the concept of exposed/not exposed properties on Entities right now therefore adding complication that it is not necessary.

If BC break for custom/contrib normalizers is not concern then we should just override normalize in EntityNormalizer. I would be as simple as

public function normalize($object, $format = NULL, array $context = []) {
    /** @var \Drupal\Core\Entity\EntityInterface $object */
    return parent::normalize(EntityAdapter::createFromEntity($object), $format, $context);
}

But then I look at ComplexDataNormalizer and it is only overriding normalize() and setting $supportedInterfaceOrClass from NormalizerBase.

So why not have EntityNormalizer extend NormalizerBase directly. Entities are not complex data in the TypedData sense(though I guess they were at one point)

Then we would just have to override normalize() in EntityNormalizer and do what ComplexDataNormalizer::normalize() did before this patch.

Then only class that will extending ComplexDataNormalizer directly FieldItemNormalizer which actually sets $supportedInterfaceOrClass to an interface that extends ComplexDataInterface.

Updating the patch for this.

UPDATE:
Just noticed the doc for ComplexDataNormalizer says


/**
 * Converts the Drupal entity object structures to a normalized array.
 *
 * This is the default Normalizer for entities....

So that is *very* wrong, but guess probably comes from the history of Entities once being and implementation of TypeData.

mradcliffe’s picture

But if there were any contrib or custom normalizers that were like EntityNormalizer in that they extended ComplexDataNormalizer, did not override normalize() and their $supportedInterfaceOrClass did not implement ComplexDataInterface they would immediately break.

This seems like a BC break.

Explaining my motivations as a contrib developer:

I have one contrib normalizer that extends ComplexDataNormalizer so that I can use its normalize method. My $supportedInterfaceOrClass doesn't extend ComplexDataInterface itself, but all the classes that implement it extend Map at some point so I think I would be okay?

I have another normalizer that extends ComplexDataNormalizer but targets my custom sub class of ItemList. I should probably just change that to ListNormalizer. I think I felt it was easier to be able to iterate over the list items and call parent::normalize. I liked that better than calling the serializer property's normalize method. Looking at things again I don't think there's a big performance hit there like I thought.

So in my case I think I would be okay with a BC break here, but I don't know about other contrib. that would be in the same spot AND be providing custom stuff. The burden would probably be mostly on Drupal site builders who may be using a custom module developed by someone else.

Status: Needs review » Needs work

The last submitted patch, 35: 2871591-35.patch, failed testing.

tedbow’s picture

@mradcliffe thanks offering a real contrib example/experience.

My $supportedInterfaceOrClass doesn't extend ComplexDataInterface itself, but all the classes that implement it extend Map at some point so I think I would be okay?

Depends with which way we go.

  1. Patch in #33 I think you be ok. Because it checks for ComplexDataInterface and handles $object not implementing it with previous logic.
  2. "Option 1" in #33 would work because ComplexDataNormalizer would be unchanged. The big downside is that only data that implements FieldItemInterface would actually get the benefit the new "exposed" properties in the normalization.
  3. Suggest in #34 would NOT work because your $object would not implement ComplexDataInterface or EntityInterface
  4. Patch in #35 would NOT work because ComplexDataNormalizer would throw an error because your $object would not implement ComplexDataInterface

So in my case I think I would be okay with a BC break here, but I don't know about other contrib. that would be in the same spot AND be providing custom stuff. The burden would probably be mostly on Drupal site builders who may be using a custom module developed by someone else.

(emphasis added)

I think also it would be on anyone that upgraded to 8.4.0(if this gets in) who is using a contrib module that does what you are describing where the module maintainer has done what you have done(which core has not suggested that you not do) and has not updated their normalizers.

I see you help maintain the TypedData documentation so I guessing you are more likely to find this issue and notice a change record than the average contrib maintainer ;)

So my guess is patch in #33 is the safest with maybe the logic handling non ComplexDataInterface objects being marked as deprecated if decide it should be.

Other follow updates would be:

  1. Update ComplexDataNormalizer phpdoc because it shouldn't refer to entities
  2. Update EntityNormalizer to extend Normalizer base directly as in #35
  3. Decide if it matters B
    asically because the way we are extending normalizer classes for any normalizer in the Serializer module we cannot guarantee that $object sent its implementation of \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize() will implement the interface specified in its $supportedInterfaceOrClass
    Even though doc for $supportedInterfaceOrClass says "The interface or class that this Normalizer supports."
dawehner’s picture

asically because the way we are extending normalizer classes for any normalizer in the Serializer module we cannot guarantee that $object sent its implementation of \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize() will implement the interface specified in its $supportedInterfaceOrClass
Even though doc for $supportedInterfaceOrClass says "The interface or class that this Normalizer supports."

Well, for me everything which is doing that, basically introduces a big in their code. I think this is one of those signs where we mixed up what single responsibility principle means. Its not about code sharing, its about having logical abstractions. Of course there is nothing to change here.

  1. +++ b/core/modules/serialization/src/Normalizer/EntityNormalizer.php
    @@ -62,4 +62,15 @@ public function denormalize($data, $class, $format = NULL, array $context = [])
    +    foreach ($object as $name => $field) {
    

    Note: This just works because \Drupal\Core\Entity\ContentEntityInterface extends \Traversable. This is not necessarily true for generic entities. To be honest there is not much we can actually do about it.

  2. +++ b/core/modules/serialization/src/Normalizer/EntityNormalizer.php
    @@ -62,4 +62,15 @@ public function denormalize($data, $class, $format = NULL, array $context = [])
    diff --git a/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php b/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php
    
    diff --git a/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php b/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php
    index decca43227..c2f62f1d59 100644
    
    index decca43227..c2f62f1d59 100644
    --- a/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php
    
    --- a/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php
    +++ b/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php
    
    +++ b/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php
    +++ b/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php
    @@ -11,6 +11,8 @@
    
    @@ -11,6 +11,8 @@
      */
     class FieldItemNormalizer extends ComplexDataNormalizer implements DenormalizerInterface {
     
    +  use ComplexDataPropertiesNormalizerTrait;
    +
       /**
        * {@inheritdoc}
        */
    

    I think we don't actually need this use statement, right?

tedbow’s picture

Status: Needs work » Needs review
FileSize
25.39 KB

Ok this is #33 rerolled plus #39.2

#35 will not work because of reasons mentioned in #38 and #33 appears to be the safest.

We can't assume that $object sent to \Drupal\serialization\Normalizer\ComplexDataNormalizer::normalize is implements ComplexDataInterface.

Status: Needs review » Needs work

The last submitted patch, 40: 2871591-40.patch, failed testing.

Wim Leers’s picture

Let's get this moving forward again?

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
878 bytes

So let's get this going again.

This fixes one of the two failures. The other failure requires \Drupal\Tests\serialization\Unit\Normalizer\TestComplexData::getProperties() to be fixed. @tedbow is best placed to fix that.

Wim Leers’s picture

tstoeckler’s picture

+++ b/core/lib/Drupal/Core/TypedData/ComplexDataInterface.php
@@ -102,4 +102,12 @@ public function toArray();
+  /**
+   * Gets an array of property objects for exposed properties.
+   *
+   * @return \Drupal\Core\TypedData\TypedDataInterface[]
+   *   The exposed properties.
+   */
+  public function getExposedProperties();

While I dislike adding something that is onely used for Rest to the generic ComplexDataInterface, I don't really see a way around it. However, it should be thoroughly documented what "exposed" in this case means and what it doesn't mean.

On a more general note, I'm not sure this is actually needed. The issue summary lists #2626924: Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata and #2825812: ImageItem should have an "derivatives" computed property, to expose all image style URLs as examples of where we want to include computed properties as part of normalized output. However, is there ever a case where we do not want that? At least by default I mean. Field types can always specify dedicated normalizers, but this issue exists to try to avoid having to do that just because you have a computed property. So I am wondering whether it wouldn't make sense to just include computed properties by default and be done with it (I guess controlled by a bc_* config value, but...). Entity reference fields have a computed entity property that could be problematic but entity references need special handling anyway (proven by the fact that both Serialization and HAL modules provide dedicated normalizers), so I don't think that's a particularly strong argument against this. (And in some cases it can even be pretty neat to have the serialized referenced entity as part of the normalized output.) Thoughts?

Status: Needs review » Needs work

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

Wim Leers’s picture

@tstoeckler note that #2626924: Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata is adding a new process_result computed property, because the existing processed computed property cannot be used due to it not containing the necessary cacheability metadata. Cacheability metadata is necessary for REST/JSON API/GraphQL responses to be cacheable.
So I'm afraid that even just that means that your proposal doesn't work. (And auto-detecting whether the value contained in a property implements CacheableDependencyInterface or not also doesn't work, because the default NULL value for empty non-required fields would then still get in the way.

Furthermore, e.g. LanguageItem's language property is computed but we wouldn't want that one — it only makes sense for internal (PHP) use. Exactly the same is true for DateTimeItem's date computed property and DateRangeItem's start_date and end_date computed properties.

In other words: there's quite a few examples of "convenience" computed properties that can only ever make sense in a PHP context. We need to somehow figure out which those are.

You could read getExposedProperties() as getNormalizableProperties() or getPropertiesThatMakeSenseOutsideOfPhp().

tedbow’s picture

@tstoeckler re #45

So I am wondering whether it wouldn't make sense to just include computed properties by default and be done with it (I guess controlled by a bc_* config value, but...). Entity reference fields have a computed entity property that could be problematic but entity references need special handling anyway

I think the problem would be if this was default any field item in contrib or custom code would automatically pick up this behavior. @Wim Leers pointed out a couple problems in core but we have no idea how this would effect contrib and custom field types because we don't know what computed properties they would be using.

tedbow’s picture

Status: Needs work » Needs review

Ok at least some the remaining errors are from changes in #2846554: Make the PathItem field type actually computed and auto-load stored aliases

Basically \Drupal\path\Plugin\Field\FieldType\PathItem::ensureLoaded() was added to make sure that all the properties were loaded when try to get any of the properties. But get() was not overridden so ensureLoaded() was not called.

This patch overrides get() and calls ensureLoaded().

tedbow’s picture

Not sure what happened there.

Patch attached

Status: Needs review » Needs work

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

tstoeckler’s picture

Not quite convinced by #47/#48. I just did the following change on a clean 8.4.x:

--- a/core/modules/serialization/src/Normalizer/ComplexDataNormalizer.php
+++ b/core/modules/serialization/src/Normalizer/ComplexDataNormalizer.php
@@ -27,7 +27,7 @@ class ComplexDataNormalizer extends NormalizerBase {
   public function normalize($object, $format = NULL, array $context = []) {
     $attributes = [];
     /** @var \Drupal\Core\TypedData\TypedDataInterface $field */
-    foreach ($object as $name => $field) {
+    foreach ($object->getProperties(TRUE) as $name => $field) {
       $attributes[$name] = $this->serializer->normalize($field, $format, $context);
     }
     return $attributes;

And did a Rest GET request before and after the patch. This is the resulting diff of the normalized output:

@@ -16,12 +16,14 @@
   ],
   "langcode": [
     {
-      "value": "en"
+      "value": "en",
+      "language": {}
     }
   ],
   "type": [
     {
       "target_id": "article",
+      "entity": {},
       "target_type": "node_type",
       "target_uuid": "f3c3f83c-e5b4-4249-a659-5bc61d98414d"
     }
@@ -35,6 +37,7 @@
   "revision_uid": [
     {
       "target_id": 1,
+      "entity": {},
       "target_type": "user",
       "target_uuid": "91374fea-8e74-4ac3-b7f5-bf2c8c7aa9df",
       "url": "\/user\/1"
@@ -54,6 +57,7 @@
   "uid": [
     {
       "target_id": 1,
+      "entity": {},
       "target_type": "user",
       "target_uuid": "91374fea-8e74-4ac3-b7f5-bf2c8c7aa9df",
       "url": "\/user\/1"
@@ -102,14 +106,17 @@
   "menu_link": [],
   "field_date": [
     {
-      "value": "2017-12-31T22:59:59"
+      "value": "2017-12-31T22:59:59",
+      "date": {}
     }
   ],
   "body": [
     {
       "value": "\u003Cp\u003Easdasdsadas\u003C\/p\u003E\r\n",
       "format": "basic_html",
-      "summary": ""
+      "processed": "\u003Cp\u003Easdasdsadas\u003C\/p\u003E",
+      "summary": "",
+      "summary_processed": ""
     }
   ],
   "comment": [

Surely the added information is not necessarily semantically sensible, but our Rest output is super verbose regardless. So basically we are adding knowledge about Rest to the core typed data system, to avoid this bit of ugliness. It is legitimately pretty ugly, so I'm not actually 100% sure that that's a bad trade-off, but I do want to make sure that everyone is aware of the trade-off we are making.

(And yes, #2626924: Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata will make that even a tad uglier, because of an additional, seemingly duplicated property, but the fact that text fields lose cacheability metadata is a bug anyway (regardless of Rest, it's just greatly exacerbated by Rest) and is neither Typed Data's nor Rest's fault, so I don't think it's a strong argument in either direction.)

tedbow’s picture

Status: Needs work » Needs review
FileSize
6.3 KB
31.87 KB

This patch fixes ComplexDataNormalizerTest for new logic.

It adds a new test function for the case where ComplexDataNormalizer is extended and the extending class has $supportedInterfaceOrClass that does not extend ComplexDataInterface as was described in real contrib situation in #36 by @mradcliffe(thanks again).

Re @tstoeckler comments #45 and #52

We can't just consider how automatically including computed properties would affect core field types. I think for BC we have to consider that contrib or custom code will have field types that have computed properties... Regardless of REST this will change normalized output for any entities that use these fields. Also what happens if there is no normalizers that support the computed properties? Right now that is not problem and contrib modules defining field types with computed properties don't have to worry about these properties going through the normalization process.

If we start to just run all computed properties through the normalization process I fear this could have unforeseen ramifications and expectations.

As stated in the issue summary

We could override this in FieldItemNormalizer to normalize all properties both computed and non-computed. But then we run into the problem that we are already using computed fields for things like the entity property in EntityReferenceItem. And we don't know what else contrib might be using for computed properties.

We could required each field type (each \Drupal\Core\Field\FieldItemInterface implementation) to specify which computed items should NOT be normalized but doing that now would be a BC break.

tstoeckler’s picture

Well for BC we could add (yet another) config setting to control this behavior, so I don't think that's a strong argument. I think we should consider how this affects fresh installs and for those I don't see how this breaks anything - it's just ugly. You're right, it would definitely need a bit more testing with contrib, but e.g. entity reference's entity property that the issue summary cites doesn't cause any problems.

Again, I don't want to shoot down the current patch I just don't feel that alternatives have been discussed sufficiently (and that hasn't changed with the latest comments).

tedbow’s picture

ok another reason to put it in typed data.
from @Wim Leers' comment on #14

That's a very interesting point! Typed Data is the Single Source of Truth, that's how REST/JSON API/GraphQL can work, that's how Schemata/OpenAPI can work. And that's indeed the point of this issue: to not put extra logic in normalizers, but instead provide additional metadata.

Putting the BC concerns aside:

If we export every computed property by default but then know that certain normalizers such as entity reference will override this behavior it will be super hard(impossible?) for any system like Schemata to know what the data coming out of Drupal will ultimately look like.

Whether the Schemata module lives on the problem remains how do we automatically information of what will be export from Drupal for REST, JSON API or other modules.

If we move this to normalization process and just allow any particular normalizers to choose this what computed properties to include it would very opaque to any systems outside of Drupal.

Below is a brainstorm of how this might be possible:
If we move this to the normalization process then to get the same effect, to know what exported data properties would available for any entity, we would have to:

  1. Add a new public function getExposedPropertes() to ComplexDataNormalizer
  2. In ComplexDataNormalizer::getExposedPropertes() return all exposed properties(if that is default behavior)
  3. Any Normalizer that wants to override behavior would override getExposedPropertes() and return the correct properties.
  4. Then Schemata/OpenAPI(or other similar modules) would have to figure out which normalizer would be used for a particular Complex data type.
  5. Then call getExposedPropertes() for the normalizer.
  6. But todo the 2 steps above you couldn't use \Symfony\Component\Serializer\Serializer::getNormalizer() because that expects a $data object and when you are doing stuff like making auto generate documentation you actually don't have a data object. You just know that type.
  7. Furthermore if you made an alterative to Serializer::getNormalizer() that didn't require a data object of the type then you also NOTcould use NormalizerInterface::supportsNormalization() because it also expects a data object.
  8. NormalizerBase::$supportedInterfaceOrClass is protected so to get information we would have to add something like NormalizerBase::supportNormalizationOfClass($className)

this is just 1 idea but I think the key problem it points to is if you just have class like \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem right now in the current normalization process it is impossible to determine which properties will be exposed

I feel like it would get very complicated very quickly if we don't use the TypeData system as the Single Source of Truth.

Wim Leers’s picture

#49: hah, funny that you found a bug in #2846554: Make the PathItem field type actually computed and auto-load stored aliases, I did too, at #2824851-73: EntityResource::patch() makes an incorrect assumption about entity keys, hence results in incorrect behavior. In that case, it was for ::equals().


#52

not semantically sensible is definitely an understatement… those entity keys can never ever ever be remotely useful. We've been going through a lot of effort to make our normalizations better (e.g. #2768651: Let TimestampItem (de)normalize to/from RFC3339 timestamps, not UNIX timestamps, for better DX). Making the output look like you describe would basically mean we say "screw any user talking to Drupal via json or hal_json :( Our normalizations may be verbose (and yes, I totally agree with that!) and not ideal (totally agree with that too!), but at least every single thing you see contains correct, actionable data. That would no longer be true. In fact, it'll literally look broken.

We're not arguing to add "REST knowledge", we're arguing to add metadata that allows code to know which properties make sense to serialize at all. This would also help e.g. https://www.drupal.org/project/default_content. It would help core REST. It would help https://www.drupal.org/project/jsonapi. It would help https://www.drupal.org/project/graphql. And it will help anything else of the like in the future. In a nutshell: it allows us to know which properties make sense to export or normalize or serialize or freeze or represent or render — whatever term you prefer, it's the same concept: the ability to take data stored in Drupal and represent it in a non-PHP-object way, so that it can be shared, stored, reused …

RE: the fact that text fields lose cacheability metadata is a bug anyway — yes it's a bug, but it's a bug that we cannot fix. Fixing it implies changing the type (i.e. from string to \Drupal\filter\FilterProcessResult), which is a BC break and therefore not allowed.


#54:

Well for BC we could add (yet another) config setting to control this behavior, so I don't think that's a strong argument.

This is inaccurate.

Today, many properties are missing in our normalizations. For TextItem, it's the processed_result property. For LinkItem, it's the options property. For ImageItem, it's image style URLs. And so on. This affects core REST (in both json and hal_json formats), as well as https://www.drupal.org/project/jsonapi, as well as https://www.drupal.org/project/graphql.

If we use getProperties(TRUE), then we get things in the output that are definitely unacceptable. And that's just core. For field types in contrib modules, we don't know what's stored in properties — there could be sensitive things in there. And there definitely will be those similar examples of things that only make sense in PHP land to normalize (like the entity property on EntityReferenceItem).

You say that BC layer would allow you to choose to keep the current behavior (which would mean no processed text, a.o.) or opt in to the new behavior (which results in clearly unusable normalizations). But … that's choosing to either not get all the data, or to get broken data. How is that a sensible choice?

just ugly is hugely euphemistic, and is ignoring the real DX consequences. It would instantly disqualify Drupal.


I completely understand your POV: you're an Entity/Field/Typed Data API maintainer. You want to keep things as simple, consistent as possible. You want to ensure that responsibilities are cleanly delineated. But … the whole reason Typed Data exists is so that there is sufficient metadata to describe the data structure, purpose, capabilities, restrictions etc of every entity field and every entity field property. I'd love to be able to use setComputed() instead of dealing with this issue, but there's #2392845: Add a trait to standardize handling of computed item lists. If setComputed() would actually mean "is calculated value" and we'd have a isConvienceProperty() for things like entity and date, then we'd be able to use that. Perhaps that's an approach that you'd like better?

That would solve 90% of our problem here!

andypost’s picture

Btw for default content it ould allow to filter user's internal fields or manage that more preciesly

dawehner’s picture

I totally agree with @tstoeckler that adding more and more stuff to the typed data stuff smells wrong. Its already an almost non understandable system, we should aim for simplicity.

  1. +++ b/core/lib/Drupal/Core/TypedData/ComplexDataWithExposedPropertiesTrait.php
    @@ -0,0 +1,27 @@
    +  public function getExposedProperties() {
    +    $properties = $this->getProperties(TRUE);
    +    /* @var  \Drupal\Core\TypedData\TypedDataInterface[] $properties */
    +    foreach (array_keys($properties) as $property_name) {
    +      if (!$properties[$property_name]->getDataDefinition()->isExposed()) {
    +        unset($properties[$property_name]);
    +      }
    +    }
    +    return $properties;
    +  }
    

    I'm always fascinated by new methods which themselves just call out to public methods. For example in this case it would not be a trait, it could be a helper living somewhere and we just call it with a complex data object. It feels like we are ignoring one dimension of programming in general.

  2. +++ b/core/lib/Drupal/Core/TypedData/DataDefinitionInterface.php
    @@ -218,4 +218,23 @@ public function getConstraint($constraint_name);
     
    +  /**
    +   * Sets the whether the data value should be exposed.
    +   *
    +   * @param bool $exposed
    +   *   Whether the data value is exposed.
    +   *
    +   * @return static
    +   *   The object itself for chaining.
    +   */
    +  public function setExposed($exposed);
    +
    +  /**
    +   * Determines whether the data value is exposed.
    +   *
    +   * @return bool
    +   *   Whether the data value is exposed.
    +   */
    +  public function isExposed();
    +
    
    Well for BC we could add (yet another) config setting to control this behavior, so I don't think that's a strong argument.

    This is inaccurate.

    I think all what tstoeckler is saying: Maybe we should put this exposed vs. not exposed level into an internal setting and get it via getSetting instead. Would that make sense?

tstoeckler’s picture

Thanks a lot @tedbow and @Wim Leers for those write-ups. They put this issue in a much better perspective for me. I will write a proper technical reply next week but need to re-read and process a bunch of things first. Just wanted to make sure you know I greatly appreciate comments like #55 and #56. I know it takes not only a lot of time but also a lot of emotional effort to argue something that you are convinced of with someone coming from a completely different angle. Thanks!

Just a quick note re #58.2: Actually what @Wim Leers thought I was saying, was in fact what I meant. As far as I know off the top of my head, typed data itself doesn't have the concept of settings, that's only on the field level. (I always get this stuff confused, though, so I may be wrong.)

dawehner’s picture

As far as I know off the top of my head, typed data itself doesn't have the concept of settings, that's only on the field level. (I always get this stuff confused, though, so I may be wrong.)

This is totally true for generic typed data, but its not true for \Drupal\Core\TypedData\ComplexDataInterface. ... which is what you can potentially iterate over. All I try is to avoid adding "yet another" thing, even well, if we want to be API first, having these ideas as a first class citizen maybe make sense.

Wim Leers’s picture

Thanks a lot @tedbow and @Wim Leers for those write-ups. They put this issue in a much better perspective for me. I will write a proper technical reply next week but need to re-read and process a bunch of things first. Just wanted to make sure you know I greatly appreciate comments like #55 and #56. I know it takes not only a lot of time but also a lot of emotional effort to argue something that you are convinced of with someone coming from a completely different angle. Thanks!

❤️

damiankloip’s picture

Status: Needs review » Needs work

I think this general approach looks good, I read the previous discussion for the reasoning (putting this at the typed data level) and agree this seems like the only real place this can live to work properly. Here are some other comments:

  1. +++ b/core/lib/Drupal/Core/TypedData/ComplexDataWithExposedPropertiesTrait.php
    @@ -0,0 +1,27 @@
    +    $properties = $this->getProperties(TRUE);
    

    I agree with Daniel here too I think - it's kind of weird when traits have a hidden dependency on the class that is using the trait.

  2. +++ b/core/lib/Drupal/Core/TypedData/ComplexDataWithExposedPropertiesTrait.php
    @@ -0,0 +1,27 @@
    +    foreach (array_keys($properties) as $property_name) {
    +      if (!$properties[$property_name]->getDataDefinition()->isExposed()) {
    +        unset($properties[$property_name]);
    +      }
    

    It could be clearer to just return the result of array_filter here?

  3. +++ b/core/lib/Drupal/Core/TypedData/DataDefinitionInterface.php
    @@ -218,4 +218,23 @@ public function getConstraint($constraint_name);
    +  public function setExposed($exposed);
    

    It would be nice if $exposed had a default value of TRUE here. It's small but being able to do setExposed() in the chain is a bit nicer.

  4. +++ b/core/lib/Drupal/Core/TypedData/ExposableDataDefinitionTrait.php
    @@ -0,0 +1,34 @@
    +    // For non-computed fields 'exposed' defaults to TRUE.
    +    if (!$this->isComputed() && !isset($this->definition['exposed'])) {
    

    This comment and the code don't match. it doesn't default it still checks the configured definition property. I think this could be condensed down to return $this->isComputed() && !empty($this->definition['exposed']) ?

  5. +++ b/core/lib/Drupal/Core/TypedData/ExposableDataDefinitionTrait.php
    @@ -0,0 +1,34 @@
    +  public function setExposed($exposed) {
    

    Same as interface comment, maybe defaults to TRUE..

  6. +++ b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php
    @@ -185,4 +185,12 @@ protected function ensureLoaded() {
    +  public function get($property_name) {
    +    $this->ensureLoaded();
    +    return parent::get($property_name);
    +  }
    

    Is this needed for this patch? Just wondering.

  7. +++ b/core/modules/serialization/src/Normalizer/ComplexDataNormalizer.php
    @@ -25,10 +28,19 @@ class ComplexDataNormalizer extends NormalizerBase {
    +    if ($object instanceof ComplexDataInterface) {
    +      $attributes = $this->normalizeProperties($object, $format, $context);
    +    }
    +    else {
    +      $attributes = [];
    +      /** @var \Drupal\Core\TypedData\TypedDataInterface $field */
    +      foreach ($object as $name => $property) {
    +        $attributes[$name] = $this->serializer->normalize($property, $format, $context);
    +      }
    

    We could have another normalizer that is just for traversable objects? we could then have our normalizers extend that? We could keep the ComplexDataNormalizer in place for anything using it already in contrib etc.

    Side note - yes when this was implemented entities did implement the ComplexDataInterface..

  8. +++ b/core/modules/serialization/src/Normalizer/ComplexDataPropertiesNormalizerTrait.php
    @@ -0,0 +1,41 @@
    +      if (is_object($attribute) && method_exists($attribute, '__toString')) {
    

    Usually normalization produces a string or array of data from objects, not objects. Looks like this is to do with the test coverage more than anything? Generally if it's an object it should have a normalizer, and normalize() would return the data that's used. This seems like kind of a hack of the normalization layer with some in-place normalization. We have a normalizer for Markup objects I think, I guess this is not one of those.

tedbow’s picture

@damiankloip thanks for the review

6. yes this needed see #49 7. This sounds like follow up because we need to cover both cases anyways in ComplexDataNormalizer because as noted elsewhere the class is already being extended in Contrib for classes that don't implement ComplexDataInterface

damiankloip’s picture

Ah sorry, I missed that comment... Does that mean we need a small addition to some test coverage for that too while we're here (sorry)? I.e. something quick to cover the case in \Drupal\Tests\path\Kernel\PathItemTest ?

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

tedbow’s picture

deleting comment, moving to #70 with correct patch

tedbow’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 66: 2871591-66.patch, failed testing. View results

tedbow’s picture

Whoops I did the patch against 8.4.

Also need to fix \Drupal\Tests\serialization\Unit\Normalizer\ComplexDataNormalizerTest

tedbow’s picture

Status: Needs work » Needs review
FileSize
16.26 KB
33.21 KB

Sorry @all that this has taken me so long to get back to.

#62
1. Remove the trait and create \Drupal\Core\TypedData\TypedDataHelper
2. fixed
3. Set default to TRUE
4. Actually I think the comment does match. If we changed it to
return $this->isComputed() && !empty($this->definition['exposed'])
Then only computed properties could ever be exposed. But all non-computed properties should be exposed unless $this->definition['exposed'] is expliciting set to TRUE.
5. fixed
6. re #64 add test coverage to PathItemTest. The trick is that for isEmpty and the 2 ways to get the property values you have to make sure that none of the other ways is called first or ->ensuredLoaded() would have already been called.
7. see #63
8. This was added to make sure it will work with logic needed for #2626924: Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata. We could remove this here and add it back there if needed(I think it will be).

Also updated ComplexDataNormalizerTest, EntityReferenceFieldItemNormalizerTest, TimestampItemNormalizerTest removed expected calls to getExposedProperites(). I created \Drupal\Tests\serialization\Unit\Normalizer\TypedDataTestTrait because all of these these tests had the same code.

Wim Leers’s picture

Status: Needs review » Needs work

Sorry for the lack of reviews here. My bad. Let's get this going again!


Q: Can we remove all the PathItem-related changes after #2905524: The count() method used on PathFieldItemList behaves differently if isEmpty() has been called. lands?

We could remove this here and add it back there if needed(I think it will be).

I think that makes sense.

Q: Can you create a change record (which will be necessary anyway), which would then also make it easier to understand and review this patch!

  1. --- a/core/lib/Drupal/Core/Entity/Plugin/DataType/EntityAdapter.php
    +++ b/core/lib/Drupal/Core/Entity/Plugin/DataType/EntityAdapter.php
    

    Unnecessary change here.

  2. +++ b/core/lib/Drupal/Core/TypedData/ExposableDataDefinitionTrait.php
    @@ -0,0 +1,34 @@
    + * Methods are implemented for \Drupal\Core\TypedData\DataDefinitionInterface.
    

    Nit: Let's add an @see?

  3. +++ b/core/lib/Drupal/Core/TypedData/ExposableDataDefinitionTrait.php
    @@ -0,0 +1,34 @@
    +    // For non-computed fields 'exposed' defaults to TRUE.
    +    if (!$this->isComputed() && !isset($this->definition['exposed'])) {
    +      return TRUE;
    +    }
    +    return !empty($this->definition['exposed']);
    

    I think the logic is correct, but perhaps a bit confusing. The following would be clearer I think:

    // Respect the definition, otherwise default to TRUE for non-computed fields.
    if (isset($this->definition['exposed']))
     {
      return $this->definition['exposed'];
    }
    else {
      return !$this->isComputed();
    }
  4. +++ b/core/lib/Drupal/Core/TypedData/ExposableDataDefinitionTrait.php
    @@ -0,0 +1,34 @@
    +   * Implements setExposed() for \Drupal\Core\TypedData\DataDefinitionInterface.
    

    Nit: this seems unnecessary?

  5. +++ b/core/lib/Drupal/Core/TypedData/TypedDataHelper.php
    @@ -0,0 +1,27 @@
    +/**
    + * Helper class for Typed Data.
    + *
    + * @ingroup typed_data
    + */
    +class TypedDataHelper {
    +
    +  /**
    +   * Gets an array exposed properties from a complex data object.
    +   *
    +   * @param \Drupal\Core\TypedData\ComplexDataInterface $data
    +   *   The complex data object.
    +   *
    +   * @return \Drupal\Core\TypedData\TypedDataInterface[]
    +   *   The exposed properties.
    +   */
    +  public static function getExposedProperties(ComplexDataInterface $data) {
    

    This feels a bit strange. I'm not sure yet how to do it better though.

  6. --- a/core/modules/path/src/Plugin/Field/FieldType/PathItem.php
    +++ b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php
    --- a/core/modules/path/tests/src/Kernel/PathItemTest.php
    +++ b/core/modules/path/tests/src/Kernel/PathItemTest.php
    

    Can't these changes happen in their own issue? Ideally this issue would only build infrastructure.

  7. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestExposedPropertyNormalizerTest.php
    @@ -0,0 +1,99 @@
    +class EntityTestExposedPropertyNormalizerTest extends EntityTestResourceTestBase {
    ...
    +    if (!FieldStorageConfig::loadByName('entity_test', 'field_test_exposed')) {
    +      // Auto-create a field for testing.
    +      FieldStorageConfig::create([
    +        'entity_type' => 'entity_test',
    +        'field_name' => 'field_test_exposed',
    +        'type' => 'exposed_string_test',
    +        'cardinality' => 1,
    +        'translatable' => FALSE,
    +      ])->save();
    +      FieldConfig::create([
    +        'entity_type' => 'entity_test',
    +        'field_name' => 'field_test_exposed',
    +        'bundle' => 'entity_test',
    +        'label' => 'Test exposed-field',
    +      ])->save();
    +    }
    

    I'd expect also a field_test_unexposed to be added here, to prove that we're not just exposing everything.

  8. +++ b/core/modules/serialization/src/Normalizer/ComplexDataNormalizer.php
    @@ -25,10 +28,19 @@ class ComplexDataNormalizer extends NormalizerBase {
    +    // $object will not always match $supportedInterfaceOrClass.
    +    // @see \Drupal\serialization\Normalizer\EntityNormalizer
    +    // Other normalizer that extend this class may only provide $object that
    +    // implements \Traversable.
    

    I first had a hard time understanding this. But it makes sense: class EntityNormalizer extends ComplexDataNormalizer, without overriding this normalize() method, therefore this may also be called with EntityInterface $object.

    IDK how to make this comment clearer. It's good enough for now.

  9. +++ b/core/modules/serialization/src/Normalizer/ComplexDataPropertiesNormalizerTrait.php
    @@ -0,0 +1,42 @@
    +      if (is_object($attribute) && method_exists($attribute, '__toString')) {
    +        $attribute = (string) $attribute;
    +      }
    

    This is weird. Why do we need this?

  10. +++ b/core/modules/serialization/tests/src/Unit/Normalizer/TypedDataTestTrait.php
    @@ -0,0 +1,36 @@
    + * Test trait that provides functions for retreiving typed data properties.
    

    Nit: s/retreiving/retrieving/

  11. +++ b/core/modules/serialization/tests/src/Unit/Normalizer/TypedDataTestTrait.php
    @@ -0,0 +1,36 @@
    +trait TypedDataTestTrait {
    

    The name is super generic. I think ExposedTypedDataTestTrait probably better?

  12. +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/DataType/ExposedStringData.php
    @@ -0,0 +1,37 @@
    +  public function getCastedValue() {
    +    return $this->getValue();
    +  }
    

    The docs for the interface say this:

      /**
       * Gets the primitive data value casted to the correct PHP type.
       *
       * @return mixed
       */
      public function getCastedValue();
    

    Therefore I think this needs to become

    return (string) $this->getValue();
    

    Which means you can probably also remove the (string) casting in ComplexDataPropertiesNormalizerTrait — i.e. my point 9 above and @damiankloip's remark in #62.8.

tedbow’s picture

Status: Needs work » Needs review
FileSize
11.57 KB
31.76 KB

1. fixed
2. added
3. fixed
4. removed
5. Previously in this patch this used be on ComplexDataInterface and used a trait for the implementation. #70.1 from suggestion of @dawehner in #58.1 and agreement from @damiankloip in #62.1 . Yeah couldn't think of a better way.
6. Removed changes. @todo Need to find where this is being fixed in other issue. this will cause the test to fail though so will have to postpone this on the other issue.
7. Actually we need to test this on the property level not the field level. This pointed to some confusing things in the test.

  • Changed ExposedStringData to ComputedStringData because it is not exposed on this level just computed, changed id to computed_string
  • Changed \Drupal\entity_test\ComputedString::__toString to return "Computed! " . $this->notComputedValue;. There is no concept of exposed here.
  • Changed ExposedPropertyTestFieldItem to have 2 properties of the computed_string, exposed_value and non_exposed_value. The only difference in the 2 is setExposed() call. This should allow testing that only difference in the properties that 1 is exposed and gets output in normalization.
  • Now the only change needed for EntityTestResourceTestBase::getExpectedNormalizedEntity(besides comments) is s/Exposed!/Computed!/. non_exposed_value doesn't get returned.

8. ok
9. See @damiankloip's #62.8 and my response #70.8 will be need for stuff like #2626924: Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata but not tested here. should we removed for now.
10. fixed
11. yes, renamed
12. Ok adding cast and removing logic mention in 9)
UPDATE: Nope can't do 9) because it breaks the cache that are expected when using \Drupal\entity_test\ComputedString because it extends BubbleableMetadata
Need to figure that out but don't have time now. Not address in this patch but will try.

Status: Needs review » Needs work

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

tedbow’s picture

+++ b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php
@@ -186,12 +186,4 @@ protected function ensureLoaded() {
-  public function get($property_name) {
-    $this->ensureLoaded();
-    return parent::get($property_name);
-  }

Removing this causes test to fail.
I created an issue just for this #2908674: When using get() method directly on PathItem the alias is not loaded. We will need to get that issue committed first but can still work on other problems here.

Looking at #71.12 and #62.8
@damiankloip's comment

Usually normalization produces a string or array of data from objects, not objects. Looks like this is to do with the test coverage more than anything? Generally if it's an object it should have a normalizer, and normalize() would return the data that's used. This seems like kind of a hack of the normalization layer with some in-place normalization. We have a normalizer for Markup objects I think, I guess this is not one of those.

In this patch I simplified ComplexDataPropertiesNormalizerTrait but had to change a couple things in a couple normalizers to get cache tags to work. I wanted to see if we can handle cache tags in a generic way for computed fields for a case like #2626924: Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata. If we can't and you have to make a bunch of specific normalizers I think it would take away a lot of the benefit of this approach.

Also including a patch with proposed changes in #2908674: When using get() method directly on PathItem the alias is not loaded to see if the test failures are from that issue.

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

Status: Needs review » Needs work

The last submitted patch, 74: 2871591-74_plus-2908674.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Wim Leers’s picture

I'll hold off reviewing this until it's green again, because it seems that some of the things I pointed at, and were changed, play a role in this patch now failing. I might be reviewing something that's going to change anyway.

tedbow’s picture

Status: Needs work » Needs review
FileSize
7.62 KB
31.2 KB

I chatted with @Wim Leers and asked if he thought it would make sense to remove the cache tags/context logic from the current test case. He agreed so this patch removes all that hopefully it will be easier to review and will stick more to is described in the title and IS.

Eventually we will need to handle caching of these computed properties for cases such as #2626924: Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata but not in this issue.
I think the previous patch proves that will be able to handle but that should be a follow up issue.

Tests will still fail because of #2908674: When using get() method directly on PathItem the alias is not loaded but this patch fixes \Drupal\Tests\serialization\Unit\Normalizer\ComplexDataNormalizerTest::testNormalizeComplexData

Status: Needs review » Needs work

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

tedbow’s picture

Status: Needs work » Needs review
FileSize
2.83 KB
30.21 KB
+++ b/core/modules/serialization/src/Normalizer/PrimitiveDataNormalizer.php
@@ -26,7 +27,7 @@ public function normalize($object, $format = NULL, array $context = []) {
-    return $object->getValue() === NULL ? NULL : $object->getCastedValue();
+    return $object->getValue() == NULL ? NULL : $object->getCastedValue();

Whoops ☹️. fixing

And coding standards fixes.

Status: Needs review » Needs work

The last submitted patch, 80: 2871591-80.patch, failed testing. View results

tedbow’s picture

Status: Needs work » Needs review
FileSize
1.37 KB
31.58 KB

Re #77 @Wim Leers' point about reviewing, lets confirm the failures are only because of #2908674: When using get() method directly on PathItem the alias is not loaded by testing the patch in #80 plus the patch in #2908674

Status: Needs review » Needs work

The last submitted patch, 82: 2871591-80-plus-2908674-2.patch, failed testing. View results

tedbow’s picture

Status: Needs work » Needs review

☹️😬😱 Random failure because of #2902191: Determine cause and fix random fail in \Drupal\Tests\settings_tray\FunctionalJavascript\SettingsTrayBlockFormTest::testBlocks. I hadn't notice that issue wasn't RTBC anymore because of rerolls, set back to RTBC.

retesting #82 but there were no other fails on that issue so I think it is ready for review.

Wim Leers’s picture

Status: Needs review » Needs work

#78: +1. I think it's important that we keep ensuring that cacheability bubbling can be layered on top without BC breaks though. So can you also open an issue for that?

  1. +++ b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
    --- a/core/modules/path/src/Plugin/Field/FieldType/PathItem.php
    +++ b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php
    
    +++ b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php
    --- a/core/modules/path/tests/src/Kernel/PathItemTest.php
    +++ b/core/modules/path/tests/src/Kernel/PathItemTest.php
    

    So these hunks will go away once #2908674: When using get() method directly on PathItem the alias is not loaded lands. Great.

  2. +++ b/core/modules/path/tests/src/Kernel/PathItemTest.php
    --- a/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php
    +++ b/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php
    

    All changes here are related to cacheability too. They should be removed from this patch — they were apparently overlooked in #78.

  3. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestExposedPropertyNormalizerTest.php
    @@ -0,0 +1,87 @@
    +    // The 'non_exposed_value' key will not be returned because setExposed(TRUE)
    ...
    +    $expected['field_test_exposed'] = [
    +      [
    +        'value' => 'value to expose',
    +        'exposed_value' => 'Computed! value to expose',
    +      ],
    +    ];
    

    The comment describes something that does not exist?

  4. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestExposedPropertyNormalizerTest.php
    @@ -0,0 +1,87 @@
    +      // Auto-create a field for testing.
    

    This comment isn't very useful. We can remove it.

  5. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestExposedPropertyNormalizerTest.php
    @@ -0,0 +1,87 @@
    +    $post_entity = parent::getNormalizedPostEntity();
    +    $post_entity['field_test_exposed'] = [
    +      [
    +        'value' => 'value to expose',
    +      ],
    +    ];
    +    return $post_entity;
    

    Can be simplified to:

    return parent::getNormalizedPostEntity()
     + [
      'field_test_exposed' => …
    ];
    

    More importantly: let's choose a value that is not the same as the value set in createEntity().

  6. +++ b/core/modules/serialization/tests/src/Unit/Normalizer/ComplexDataNormalizerTest.php
    @@ -7,8 +7,8 @@
    +use Drupal\Core\Cache\CacheableDependencyInterface;
    

    We don't need this anymore, see #78 again.

  7. +++ b/core/modules/system/tests/modules/entity_test/src/ComputedString.php
    @@ -0,0 +1,37 @@
    +class ComputedString {
    
    +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/DataType/ComputedStringData.php
    @@ -0,0 +1,37 @@
    +class ComputedStringData extends StringData {
    

    If we'd have that follow-up that #78 describes (and which I asked for earlier in this comment), which would layer on cacheability bubbling, then the structure of this code will make far more sense too! :)

tedbow’s picture

@Wim Leers thanks for the review!

Created follow #2910211: Allow computed exposed properties in ComplexData to support cacheability.

  1. yep this patch will remove that for easier review. so starting from #80 again.
  2. PathItemTest.php is there because I added the patch for #2908674: When using get() method directly on PathItem the alias is not loaded I removed in this patch. removing ResourceResponseSubscriber.php changes.
  3. Changed the comment to refer to the property in the test field. Part of what we are testing here is that all computed fields don't get returned here. if we just test for exposed properties then can't be sure non-exposed properties won't also be returned.
  4. removed
  5. Fixed
  6. Removed
  7. Ok, not sure if you wanted a change here. We still need a computed field regardless of whether we are dealing with catchability here. No change

Note, the interdiff is from #80 because #82 was just to prove test failures are because of #2908674: When using get() method directly on PathItem the alias is not loaded so this should 6 fails like #80

Status: Needs review » Needs work

The last submitted patch, 86: 2871591-86.patch, failed testing. View results

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
1.11 KB
27.41 KB
28.91 KB
  1. Oops, didn't mention intend to mention PathItemTest in this point! I was indeed pointing to ResourceResponseSubscriber.
  1. Nope, didn't want a change, was merely observing :)

#86 failed because:

  1. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestExposedPropertyNormalizerTest.php
    @@ -0,0 +1,87 @@
    +  protected function getNormalizedPostEntity() {
    

    This now has weird indentation… which was also hiding a functional change, which also caused failures.

  2. it didn't include #2908674: When using get() method directly on PathItem the alias is not loaded — EDIT: apparently this was intentional, I first missed the last paragraph of Ted's last comment.

The last submitted patch, 88: 2871591-88.patch, failed testing. View results

Wim Leers’s picture

Trying to do some really comprehensive reviews to get this one to an RTBC'able point.

  1. +++ b/core/lib/Drupal/Core/Field/FieldConfigBase.php
    @@ -6,12 +6,14 @@
     abstract class FieldConfigBase extends ConfigEntityBase implements FieldConfigInterface {
     
    +  use ExposableDataDefinitionTrait;
    

    Doesn't this also mean that the config schema should be expanded? i.e. field.schema.yml

  2. +++ b/core/lib/Drupal/Core/TypedData/DataDefinitionInterface.php
    @@ -218,4 +218,23 @@ public function getConstraint($constraint_name);
    +   *   Whether the data value is exposed.
    

    s/is/should be/

  3. +++ b/core/lib/Drupal/Core/TypedData/ExposableDataDefinitionTrait.php
    @@ -0,0 +1,36 @@
    +/**
    + * Exposable property methods.
    + *
    + * Methods are implemented for \Drupal\Core\TypedData\DataDefinitionInterface.
    + *
    + * @see \Drupal\Core\TypedData\DataDefinitionInterface
    + */
    

    Changing this comment to be similar to what \Drupal\Core\Plugin\Definition\DependentPluginDefinitionTrait and \Drupal\system\Tests\Entity\EntityDefinitionTestTrait.

  4. +++ b/core/lib/Drupal/Core/TypedData/TypedDataHelper.php
    @@ -0,0 +1,27 @@
    +   * @return \Drupal\Core\TypedData\TypedDataInterface[]
    +   *   The exposed properties.
    

    This does not document what the keys are. Done.

  5. +++ b/core/lib/Drupal/Core/TypedData/TypedDataHelper.php
    @@ -0,0 +1,27 @@
    +  public static function getExposedProperties(ComplexDataInterface $data) {
    

    This is only used in one place. Which means it doesn't make sense for this to be in a separate "helper" class. We can still extract it later.

    (In #70, it used to be necessary, because many other things called this, but that's no longer the case. So we can now simplify this, yay!)

    Done.

  6. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestExposedPropertyNormalizerTest.php
    @@ -0,0 +1,87 @@
    +  protected static $mimeType = 'application/json';
    

    This is testing the json format. We should also test hal_json.

  7. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestExposedPropertyNormalizerTest.php
    @@ -0,0 +1,87 @@
    +    $post_entity = parent::getNormalizedPostEntity() + [
    

    Nit: This can be simplified a bit more still.

  8. +++ b/core/modules/serialization/src/Normalizer/ComplexDataNormalizer.php
    @@ -25,10 +28,19 @@ class ComplexDataNormalizer extends NormalizerBase {
    +    // Other normalizer that extend this class may only provide $object that
    

    s/normalizer/normalizers/

    Fixed.

  9. +++ b/core/modules/serialization/src/Normalizer/ComplexDataPropertiesNormalizerTrait.php
    @@ -0,0 +1,34 @@
    + * Normalization methods for complex data properties.
    

    This is accurate, but an @see would make it clear for which classes this can be used. Done.

  10. +++ b/core/modules/serialization/src/Normalizer/TypedDataNormalizer.php
    @@ -18,7 +20,11 @@ class TypedDataNormalizer extends NormalizerBase {
    -    return $object->getValue();
    +    $value = $object->getValue();
    +    if (is_object($value) && $object instanceof PrimitiveInterface) {
    +      return $object->getCastedValue();
    +    }
    +    return $value;
    

    Why do we need this change?

    Why doesn't \Drupal\serialization\Normalizer\PrimitiveDataNormalizer already cover this?

  11. +++ b/core/modules/serialization/tests/src/Unit/Normalizer/ComplexDataNormalizerTest.php
    @@ -44,103 +45,77 @@ protected function setUp() {
    +    $complexData = $this->prophesize(ComplexDataInterface::class)->reveal();
    

    Nit: we don't do camelCase in variables, only on class properties.

    Fixed.

  12. +++ b/core/modules/serialization/tests/src/Unit/Normalizer/ExposedTypedDataTestTrait.php
    @@ -0,0 +1,36 @@
    + * Test trait that provides functions for retrieving typed data properties.
    

    Nit: missing "exposed".

  13. +++ b/core/modules/serialization/tests/src/Unit/Normalizer/ExposedTypedDataTestTrait.php
    @@ -0,0 +1,36 @@
    +trait ExposedTypedDataTestTrait {
    

    Nit: missing "properties". Should be named ExposedTypedDataPropertiesTestTrait

  14. +++ b/core/modules/system/tests/modules/entity_test/config/schema/entity_test.data_types.schema.yml
    @@ -0,0 +1,15 @@
    +# Schema for the configuration of the exposed string field type.
    
    +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/ExposedPropertyTestFieldItem.php
    @@ -0,0 +1,45 @@
    + * Defines the 'Exposed Property' entity test field type.
    ...
    + *   id = "exposed_string_test",
    + *   label = @Translation("Exposed Property (test)"),
    ...
    +class ExposedPropertyTestFieldItem extends StringItem {
    

    "exposed property" vs "exposed string test" vs "exposed property (test)" vs "exposed property test" vs "exposed string".

    Let's make this consistent.

    I propose:

    • @FieldType = exposed_property_test
    • @DataType = computed_string_test
  15. +++ b/core/modules/system/tests/modules/entity_test/config/schema/entity_test.data_types.schema.yml
    @@ -0,0 +1,15 @@
    +field.storage_settings.exposed_string_test:
    +  type: mapping
    +  label: 'String settings'
    +  mapping:
    +    max_length:
    +      type: integer
    +      label: 'Maximum length'
    +    case_sensitive:
    +      type: boolean
    +      label: 'Case sensitive'
    +    is_ascii:
    +      type: boolean
    +      label: 'Contains US ASCII characters only'
    

    Rather than specifying these explicitly, we can extend field.storage_settings.string. (field.storage_settings.uri also does this, for example.)

    Done.

  16. +++ b/core/modules/system/tests/modules/entity_test/config/schema/entity_test.data_types.schema.yml
    --- /dev/null
    +++ b/core/modules/system/tests/modules/entity_test/src/ComputedString.php
    

    I'd expect this class to be living in the same directory as the @DataType plugin that uses it.

  17. +++ b/core/modules/system/tests/modules/entity_test/src/ComputedString.php
    @@ -0,0 +1,37 @@
    +class ComputedString {
    

    Should be marked @internal explicitly, because it is coupled to a @DataType plugin — it's an implementation detail of that.

  18. +++ b/core/modules/system/tests/modules/entity_test/src/ComputedString.php
    @@ -0,0 +1,37 @@
    +  protected $notComputedValue;
    

    I think $sourceString or $sourceValue would be a better name.

  19. +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/DataType/ComputedStringData.php
    @@ -0,0 +1,37 @@
    + * The exposed string test data type.
    ...
    + *   label = @Translation("Exposed String")
    

    This is not exposed, but computed. Fixed.

  20. +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/DataType/ComputedStringData.php
    @@ -0,0 +1,37 @@
    + * This simply concatenates 'Exposed! ' before the 'value' property.
    

    This comment does not belong here anymore. Removed.

  21. +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/ExposedPropertyTestFieldItem.php
    @@ -0,0 +1,45 @@
    + *   description = @Translation("A field containing two computed string values, one exposed and one not exposed."),
    

    one string, and two computed string values

  22. +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/ExposedPropertyTestFieldItem.php
    @@ -0,0 +1,45 @@
    +      ->setLabel(new TranslatableMarkup('Text value exposed'))
    ...
    +      ->setLabel(new TranslatableMarkup('Text value non-exposed'))
    

    Nit: These are computed. The labels imply otherwise. Fixed.

I only did string fixes and removed the TypedDataHelper class, and merged its logic in the sole caller. In other words: only trivial things, so I can continue to review this.

Wim Leers’s picture

Status: Needs review » Needs work

Of the 22 remarks in #90, I only addressed #2, #3, #4, #5, #7, #8, #9, #11, #12, #13, #15, #19, #20, #21, #22.

Still to be done: #1, #6, #10, #14, #16, #17, #18. Leaving those to @tedbow.

Wim Leers’s picture

Title: Allow ComplexData in TypedData to specify computed properties that should be exported in normalization and other contexts. » Allow ComplexData in TypedData to specify computed properties that should be exposed in normalization and other contexts

Updating issue title to match current patch.

Wim Leers’s picture

I said this in #90.#6:

This is testing the json format. We should also test hal_json.

You may wonder "why?". In general, you'd be right, this should not be necessary. But:

  1. we're modifying core/modules/hal/src/Normalizer/FieldItemNormalizer.php and core/modules/serialization/src/Normalizer/ComplexDataNormalizer.php to use ComplexDataPropertiesNormalizerTrait
  2. the updated core/modules/serialization/src/Normalizer/ComplexDataNormalizer.php ensures that both \Drupal\serialization\Normalizer\EntityNormalizer and \Drupal\serialization\Normalizer\FieldItemNormalizer inherit this, because both extend \Drupal\serialization\Normalizer\ComplexDataNormalizer
  3. but the HAL module has its own field item normalizer which doesn't extend \Drupal\serialization\Normalizer\ComplexDataNormalizer, and therefore it uses the trait itself too

Therefore, since the code paths are different, we need to explicitly test both formats.


Finally, having written this, I just realized something: shouldn't we expand the test coverage, to not only test exposed vs non-exposed properties on fields, but also exposed vs non-exposed fields on entities?

tedbow’s picture

Finally, having written this, I just realized something: shouldn't we expand the test coverage, to not only test exposed vs non-exposed properties on fields, but also exposed vs non-exposed fields on entities?

I chatted with @Wim Leers and he realized this is not the case but for others

We don't need to worry about exposed/non-exposed field on entities because the changes in \Drupal\Core\TypedData\DataDefinitionInterface::setExposed allow us to expose/not expose properties on fields to normalization but don't allow us to do the same thing for fields on entities.

Ok actually maybe I wrong but looking what I just said about fields on entities and then looking at @Wim Leers comment #90.1 I think there might be bigger problems with this approach.

Doesn't this also mean that the config schema should be expanded? i.e. field.schema.yml

Yes it would mean that it all would mean that this this would valid code

$field = FieldConfig::create([
      'entity_type' => $this->entityType,
      'bundle' => $this->bundle,
      'field_name' => 'body',
      'label' => 'Body',
    ]);
$field->setExposed(TRUE);

But course that doesn't actually do anything because field config doesn't have a concept of exposed or not.

so now we would have add the exposed methods to FieldItemDataDefinition, EntityDataDefinition, among a whole bunch of the others but we are only calling isExposed() in \Drupal\serialization\Normalizer\ComplexDataPropertiesNormalizerTrait::getExposedProperties so everywhere else exposed doesn't actually do anthing
😦😖

I am missing something here? So probably adding to DataDefinitionInterface is not the right answer.

Looking back I added this #10 because it seems like a better idea to expose properties on the Typed Data level.

Before that I had suggested in #2 that we add a $export_computed_properties to FieldType that FieldType plugins could add to their annotations. Is that a better way?

tedbow’s picture

Talked with @fago, @amateescu, @dawehner, @Wim Leers and @xjm at Drupalcon Vienna

Fago suggested not using "exposed" but instead use the term "internal".

He also suggested so only add the new getter isExposed() onto DataDefinitionInterface not the setter as this intention(see @Wim Leers question #27.2). So not everything class that implements DataDefinitionInterface will be able to be set to internal. This is will be similar to "computed"

We have \Drupal\Core\TypedData\DataDefinitionInterface::isComputed but setComputed is not defined on this interface. It is defined in \Drupal\Core\TypedData\DataDefinition::setComputed.

So this takes care of the problem #94. In the example I mentioned we will not be able to call setInternal() on FieldConfig objects. Just call isInternal() which will always be false because you can't call setInternal or setComputed on FieldConfig.

If later we want to introduce the concept of internal fields we can add setInternal at this level.

So not uploading a patch now but here the steps that we will need to take.

  1. Remove \Drupal\Core\TypedData\DataDefinitionInterface::setExposed
  2. Add setExposed() to \Drupal\Core\TypedData\DataDefinition
  3. Remove setExposed from ExposableDataDefinitionTrait
  4. Remove ExposableDataDefinitionTrait from \Drupal\Core\Field\FieldConfigBase

Might be other things but that is all I can think of now.

Wim Leers’s picture

Issue tags: +Vienna2017
tedbow’s picture

Status: Needs work » Needs review
FileSize
20.47 KB
25.78 KB

Here is the patch the changes described in #95

Wim Leers’s picture

  1. +++ b/core/lib/Drupal/Core/TypedData/DataDefinition.php
    @@ -354,4 +354,18 @@ public function __sleep() {
    +  public function setInternal($internal = TRUE) {
    

    ❓ Should not default to TRUE because \Drupal\Core\TypedData\DataDefinition::setReadOnly() and \Drupal\Core\TypedData\DataDefinition::setRequired() also don't do this.

  2. +++ b/core/lib/Drupal/Core/TypedData/DataDefinitionInterface.php
    @@ -219,22 +219,11 @@ public function getConstraint($constraint_name);
    -  public function setExposed($exposed = TRUE);
    ...
    -  public function isExposed();
    +  public function isInternal();
    

    👍 This removes the setter from the interface, keeping it only on the ipmlementation.

  3. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestInternalPropertyNormalizerTest.php
    @@ -7,11 +7,11 @@
    - * Test that exposed properties are actually exposed in REST.
    + * Test that internal properties are not exposed in REST.
    

    👍 This is the only remaining occurrence of the string "expose" in the entire patch!

  4. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestInternalPropertyNormalizerTest.php
    @@ -30,13 +30,14 @@ class EntityTestExposedPropertyNormalizerTest extends EntityTestResourceTestBase
    +    // The 'internal_value' property in test field type will not return in
    +    // normalization because setInternal(FALSE) was not called for this
    

    s/will not return in normalization/is not exposed in the normalization/

  5. +++ b/core/lib/Drupal/Core/Field/FieldConfigBase.php
    @@ -6,12 +6,15 @@
    +  use InternalDataDefinitionTrait;
    

    👍 This adds the isInternal() method implementation from the trait. Because there is no setter available on FieldConfigBase and subclasses (it only lives on DataDefinition and subclasses, which FieldConfigBase does not extend), this means that it will fall back to calling isComputed(), which is hardcoded to always return FALSE in \Drupal\field\Entity\FieldConfig::isComputed().

    ⁉️ However, the other subclass of FieldConfigBase, BaseFieldOverride, implements ::isComputed() like this:

      public function isComputed() {
        return $this->getBaseFieldDefinition()->isComputed();
      }
    

    Which means that if a base field marks itself as internal (via setInternal(TRUE), because \Drupal\Core\Field\BaseFieldDefinition extends DataDefinition), then that will continue to be respected.

    The only example of an entity type that has a computed base field in core is \Drupal\entity_test\Entity\EntityTestComputedField.

    @fago was very explicit in wanting it on DataDefinitionInterface, meaning that he knew it'd affect base fields. Which means we must also respect isInternal() for fields, not just for properties. Which means we'll also need to update \Drupal\serialization\Normalizer\ContentEntityNormalizer::normalize() to use ComplexDataPropertiesNormalizerTrait::normalizeProperties(), to ensure computed fields are also omitted from the normalization, unless they're explicitly marked as non-internal.

    This also means we should add REST test coverage for \Drupal\entity_test\Entity\EntityTestComputedField, because right now its computed field (and hence internal by default) is being exposed, it should not be.

Also pinged @fago for feedback here, that's why I'm leaving this at Needs review.

tedbow’s picture

Assigned: Unassigned » tedbow
FileSize
28.88 KB
9.91 KB

re @Wim Leers comments on #91 regarding his review in #90

Still to be done: #1, #6, #10, #14, #16, #17, #18. Leaving those to @tedbow.

1. Now with the in #97 this will no longer be necessary because you can't set internal on fields. So like
computed it is not needed in the schema.
6. added EntityTestHalJsonInternalPropertyNormalizerTest
10. No this fail if removed. I still need to figure out why @todo figure this out.
14. fixed
16. moved
17. Marked
18. Changed to $sourceString

Didn't see #98 when I did this. will now address those.

Status: Needs review » Needs work

The last submitted patch, 99: 2871591-99.patch, failed testing. View results

tedbow’s picture

Status: Needs work » Needs review
FileSize
1.6 KB
28.87 KB

#98 review
1. fixed
2. 👍
3. 👍
4. fixed
5. 👍
6.

Which means we'll also need to update \Drupal\serialization\Normalizer\ContentEntityNormalizer::normalize() to use ComplexDataPropertiesNormalizerTrait::normalizeProperties(), to ensure computed fields are also omitted from the normalization, unless they're explicitly marked as non-internal.

Won't this be a BC break for our REST responses?
Because if we do

This also means we should add REST test coverage for \Drupal\entity_test\Entity\EntityTestComputedField, because right now its computed field (and hence internal by default) is being exposed, it should not be.

That means any computed fields outside of core that are being returned REST responses will all of sudden not be returned.

Wim Leers’s picture

Won't this be a BC break for our REST responses?

That means any computed fields outside of core that are being returned REST responses will all of sudden not be returned.

Ugh, you're right :( What a mess. This means we'll have to treat fields and properties inconsistently, for BC reasons. Right?

dawehner’s picture

This looks seriously great!

  1. +++ b/core/lib/Drupal/Core/TypedData/InternalDataDefinitionTrait.php
    @@ -0,0 +1,26 @@
    + * Trait for data definitions that have test for internal properties.
    

    For some reason this doesn't sound like proper english, anyone may clarify?

  2. +++ b/core/modules/hal/tests/src/Functional/EntityResource/EntityTest/EntityTestHalJsonInternalPropertyNormalizerTest.php
    @@ -0,0 +1,97 @@
    +      ])->save();
    +
    +    }
    
    +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestJsonInternalPropertyNormalizerTest.php
    @@ -0,0 +1,88 @@
    +      ])->save();
    +
    +    }
    

    🙃 lonely empty line

tedbow’s picture

Ugh, you're right :( What a mess. This means we'll have to treat fields and properties inconsistently, for BC reasons. Right?

Yes so I think we have to have all fields be external unless they are explicitly set to internal.

So this patch overrides isInternal() in \Drupal\Core\Field\BaseFieldDefinition() to implement that

Which means we'll also need to update \Drupal\serialization\Normalizer\ContentEntityNormalizer::normalize() to use ComplexDataPropertiesNormalizerTrait::normalizeProperties()

It also does this but because ContentEntityInterface does not implement ComplexDataInterface which \Drupal\serialization\Normalizer\ComplexDataPropertiesNormalizerTrait::normalizeProperties expects we have to actually use \Drupal\Core\Entity\Plugin\DataType\EntityAdapter::createFromEntity.

This also means we have to add access logic to ComplexDataPropertiesNormalizerTrait::normalizeProperties() to handle field access.

It seemed to work the content entities I tested but lets see.

re @dawehner's review in #103

This looks seriously great!

👍
1. fixed
2.

🙃 lonely empty line

This lines weren't actually lonely, they just like being by themselves sometime but sadly we have to conform to standards... so removed.

Status: Needs review » Needs work

The last submitted patch, 104: 2871591-104.patch, failed testing. View results

damiankloip’s picture

Overall, very much liking where this is headed! I think it's pretty much where we want to be. Great work pushing this forward Ted. A few comments:

  1. +++ b/core/lib/Drupal/Core/TypedData/DataDefinition.php
    @@ -352,4 +354,18 @@ public function __sleep() {
    +    $this->definition['internal'] = $internal;
    

    Just to be pedantic, we could cast to a bool here.

  2. +++ b/core/lib/Drupal/Core/TypedData/InternalDataDefinitionTrait.php
    @@ -0,0 +1,26 @@
    +    else {
    +      return $this->isComputed();
    +    }
    

    This is mainly a preference thing I guess, but we can just ditch the else here. The return value can just be the default return for this method.

  3. +++ b/core/modules/hal/tests/src/Functional/EntityResource/EntityTest/EntityTestHalJsonInternalPropertyNormalizerTest.php
    @@ -0,0 +1,96 @@
    +        'non_internal_value' => 'Computed! value to be internal',
    

    non internal?

  4. +++ b/core/modules/hal/tests/src/Functional/EntityResource/EntityTest/EntityTestHalJsonInternalPropertyNormalizerTest.php
    @@ -0,0 +1,96 @@
    +    if (!FieldStorageConfig::loadByName('entity_test', 'field_test_internal')) {
    

    Howcome we need this? when is the field already there in the test env?

  5. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestJsonInternalPropertyNormalizerTest.php
    @@ -0,0 +1,87 @@
    +        'non_internal_value' => 'Computed! value to be internal',
    

    Same here

  6. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTestInternalField/EntityTestJsonInternalFieldTestBase.php
    @@ -0,0 +1,167 @@
    +  use AnonResourceTestTrait;
    +
    +  use BcTimestampNormalizerUnixTestTrait;
    

    nit: we can just have one use statement to define multiple here.

  7. +++ b/core/modules/serialization/src/Normalizer/ComplexDataNormalizer.php
    @@ -25,10 +29,19 @@ class ComplexDataNormalizer extends NormalizerBase {
    +      foreach ($object as $name => $property) {
    +        $attributes[$name] = $this->serializer->normalize($property, $format, $context);
    +      }
    

    This is where I mean access would now not be checked on the property level, compared to the normalizeProperties methods from the new trait.

  8. +++ b/core/modules/serialization/src/Normalizer/ComplexDataPropertiesNormalizerTrait.php
    @@ -0,0 +1,55 @@
    +      if ($property instanceof AccessibleInterface && !$property->access('view', $context['account'])) {
    +        continue;
    +      }
    

    Hmm, this is introducing access checking we don't currently have. Not sure we should do that here? Question is, does this mean we then need to make the same change for regular iterations outside of this complex data case (the madness we have with some extending classes not implementing ComplexDataInterface)?

  9. +++ b/core/modules/serialization/src/Normalizer/TypedDataNormalizer.php
    @@ -18,7 +20,11 @@ class TypedDataNormalizer extends NormalizerBase {
    +    $value = $object->getValue();
    +    if (is_object($value) && $object instanceof PrimitiveInterface) {
    +      return $object->getCastedValue();
    +    }
    +    return $value;
    

    Howcome we need this change? This is doing the same as the PrimitiveDataNormalizer I think? Which is meant to be running before this one.

tedbow’s picture

Status: Needs work » Needs review
FileSize
1.73 KB
37.42 KB

I made a namespace c/p error in EntityTestInternalFieldJsonAnonTest. It will probably still fail with testPatch but should fix the fatal error.
ContentEntityNormalizerTest will fail again b/c it needs to be updated to take in account use of EntityAdapter.

Status: Needs review » Needs work

The last submitted patch, 107: 2871591-106.patch, failed testing. View results

amateescu’s picture

As we discussed in Vienna, this 'internal' approach makes very much sense! Here's a high-level review for now:

  1. +++ b/core/lib/Drupal/Core/TypedData/InternalDataDefinitionTrait.php
    @@ -0,0 +1,26 @@
    +trait InternalDataDefinitionTrait {
    

    Is this trait really needed? It only saved a few lines of code at the expense of making it harder to scan various classes for what the isInternal() method does.

    I kind of like how every setter and getter stand together in \Drupal\Core\TypedData\DataDefinition for example, the code is super easy to read.

  2. +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestInternalField.php
    @@ -0,0 +1,41 @@
    +class EntityTestInternalField extends EntityTest {
    

    It would be better if we didn't create an entirely new entity type for these two base fields and add them via entity_test_entity_base_field_info() instead.

  3. +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestInternalField.php
    @@ -0,0 +1,41 @@
    +    $fields['non_internal_string_field'] = BaseFieldDefinition::create('string')
    +      ->setLabel('Internal');
    

    Small contradiction here :)

  4. +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/DataType/ComputedStringData.php
    @@ -0,0 +1,34 @@
    +class ComputedStringData extends StringData {
    
    +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/InternalPropertyTestFieldItem.php
    @@ -0,0 +1,45 @@
    +      ->setComputed(TRUE)
    +      ->setClass(ComputedStringData::class)
    ...
    +      ->setComputed(TRUE)
    +      ->setClass(ComputedStringData::class);
    

    We don't have any other test data types, so I wonder why is this one special/needed.

    It seems to only return some concatenated values after all, which is also not dependent on the setComputed(TRUE) definition from the second hunk.

Wim Leers’s picture

Thanks for the review, @amateescu! ❤️

  1. I like that.
  2. Ohh, interesting! Then we could just expand \Drupal\Tests\rest\Functional\EntityResource\EntityTest\EntityTestResourceTestBase, instead of adding lots of new test classes. I like it.
  3. Well-spotted!
  4. Because we want to have explicit test coverage for a field that has a computed property with both the default (internal=true) and the alternative (internal=false). If we don't have a test data type, we can't explicitly test that things are working the way they should. The thing it computes is irrelevant (and yes, it's silly/contrived, but that's because it's only for testing purposes), the normalizer' behavior is what we want to test, and we need this for that.
    Or do you see some better/more elegant way to test this?
tedbow’s picture

Status: Needs work » Needs review
FileSize
11.31 KB
31.83 KB

@amateescu thanks for the review and the help in Vienna!
#109
1. Sounds good, removed
2. Ok added this testing to EntityTestResourceTestBase which removes the need for a specific test entity type.
Also updated entity_test_entity_base_field_info () to add the base field. I wondering though since I am adding the basefield when $entity_type->id() === 'entity_test' should we just be adding it in \Drupal\entity_test\Entity\EntityTest::baseFieldDefinitions.

Also I am not adding the basefield non_internal_string_field anymore because it is just a field that setInternal() is not called on. This the same as all the other basefields.
3. Removed this code because not using this basefield anymore see 2)
4. what @Wim Leers said in #110.4

tedbow’s picture

Assigned: tedbow » Unassigned

Unassigning from myself for further reviews. Feel to assign back to me if/when it needs work.

Wim Leers’s picture

Also updated entity_test_entity_base_field_info () to add the base field. I wondering though since I am adding the basefield when $entity_type->id() === 'entity_test' should we just be adding it in \Drupal\entity_test\Entity\EntityTest::baseFieldDefinitions.

We shouldn't add it unconditionally in entity_test_entity_base_field_info(), but only if \Drupal::state()->get('entity_test.test_internal'). We'd then set that state in our test coverage. In other words: we should only add this field for our specific test!

Status: Needs review » Needs work

The last submitted patch, 111: 2871591-111.patch, failed testing. View results

tedbow’s picture

Status: Needs work » Needs review
FileSize
1.71 KB
32.24 KB

@Wim Leers I have tried your suggestion in #113. I am doing something wrong because the column is not being recognized.

If I check for \Drupal::state()->get('entity_test.test_internal') I can never get it to work.
I always get this error

1) Drupal\Tests\rest\Functional\EntityResource\EntityTest\EntityTestJsonAnonTest::testGet
Drupal\Core\Entity\EntityStorageException: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'internal_string_field' in 'field list': INSERT INTO {entity_test} (type, uuid, langcode, name, created, user_id, internal_string_field) VALUES (:db_insert_placeholder_0, :db_insert_placeholder_1, :db_insert_placeholder_2, :db_insert_placeholder_3, :db_insert_placeholder_4, :db_insert_placeholder_5, :db_insert_placeholder_6); Array
(
[:db_insert_placeholder_0] => entity_test
[:db_insert_placeholder_1] => 9cfaa6c4-a9c3-4343-88dd-8f3b5497785d
[:db_insert_placeholder_2] => en
[:db_insert_placeholder_3] => Llama
[:db_insert_placeholder_4] => 1507810287
[:db_insert_placeholder_5] => 0
[:db_insert_placeholder_6] => value to be internal
)

/var/www/d8_3_rest/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php:805
/var/www/d8_3_rest/core/lib/Drupal/Core/Entity/Entity.php:387
/var/www/d8_3_rest/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php:72
/var/www/d8_3_rest/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php:158

I have tried running $this->container->get('state')->set('entity_test.test_internal', TRUE); at a lot of different points. But nothing works.

Status: Needs review » Needs work

The last submitted patch, 115: 2871591-115.patch, failed testing. View results

borisson_’s picture

I was trying to review this but I don't fully understand what's going on, however there were some small things that could be improved, so attached is patch that fixes my own nits.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
31.88 KB
1.09 KB

@tedbow asked me to look into #115 — some debugging later, ended up with this.

The last submitted patch, 118: allow_complexdata_in-2871591-118.patch, failed testing. View results

dagmar’s picture

+++ b/core/lib/Drupal/Core/TypedData/DataDefinitionInterface.php
@@ -218,4 +218,12 @@ public function getConstraint($constraint_name);
+  /**
+   * Determines whether the data value is internal.
+   *
+   * @return bool
+   *   Whether the data value is internal.
+   */
+  public function isInternal();

This is introducing this concept of isInternal. I think it would be nice to see in the documentation of this method what means to have a internal value. As an example, take a look what ckeditor says about isInternal

Wim Leers’s picture

#120++

Wim Leers’s picture

I investigated the failing tests.

First note only tests for the hal_json format are failing, those for json are passing. That's very interesting.
Then, note that #111 removed the custom entity type for testing, in favor of adding a internal_string_field field to the EntityTest entity type. But it did not update the expected normalized entity. Because it's a field that's specifically intended to not be normalized, because it's internal:

+      // Set a value for the internal field to confirm that it will not be
+      // returned in normalization.

Or, as @tedbow put it in #104:

Yes so I think we have to have all fields be external unless they are explicitly set to internal.

Clearly this is working for json, but not hal_json.

tedbow’s picture

+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php
@@ -53,11 +53,10 @@ protected function setUpAuthorization($method) {
-    $this->container->get('entity_type.manager')->clearCachedDefinitions();
+    \Drupal::entityDefinitionUpdateManager()->applyUpdates();

@Wim Leers thanks for figuring this out!

Clearly this is working for json, but not hal_json.

Yep this patch has updated \Drupal\hal\Normalizer\ContentEntityNormalizer yet so it won't work.

This patch fixes that.

The problem with using \Drupal\serialization\Normalizer\ComplexDataPropertiesNormalizerTrait::normalizeProperties in \Drupal\hal\Normalizer\ContentEntityNormalizer::normalize is that it first filters the fields by $context['included_fields'].
We could add the logic inside the trait but I think this is hal only concept.
We could call normalizeProperties() properties first and then filter but then we have normalized fields that we won't be returning.

So in this patch I have made \Drupal\serialization\Normalizer\ComplexDataPropertiesNormalizerTrait::getNonInternalAccessibleProperties() public and added the AccessibleInterface logic into it so that \Drupal\serialization\Normalizer\ComplexDataPropertiesNormalizerTrait::normalizeProperties can use this directly.

which this brings up from #90

I only did string fixes and removed the TypedDataHelper class, and merged its logic in the sole caller.

So we could add back TypedDataHelper and move getNonInternalAccessibleProperties(). Since it is called 2 places. But it might to get rid of getNonInternalAccessibleProperties() altogether now. Since \Drupal\serialization\Normalizer\ComplexDataPropertiesNormalizerTrait::normalizeProperties is very thin now and we could just use getNonInternalAccessibleProperties() in all the normalizers directly.

But not doing that in this patch.

Status: Needs review » Needs work

The last submitted patch, 123: 2871591-123.patch, failed testing. View results

tedbow’s picture

Status: Needs work » Needs review
FileSize
4.49 KB
36.44 KB

So yay now the only test that is not passing is ContentEntityNormalizerTest

That makes sense because we have changed to use EntityAdapter so the expected method calls would be different.

Looking at how to mock EntityAdapter I was 😦. Then I found \Drupal\Core\Entity\Entity::getTypedData and I was like 😅.

That is much easier to test 🙌.

So this fixes ContentEntityNormalizerTest for its current test cases. I am not adding additional test case for checking internal fields. But this will need to be added.

dawehner’s picture

+++ b/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php
@@ -72,22 +76,14 @@ public function normalize($entity, $format = NULL, array $context = []) {
+    $field_items = static::getNonInternalAccessibleProperties($entity->getTypedData(), $context);
...
+      $field_items = array_filter($field_items, function (FieldItemListInterface $field_item) use ($context) {
+        return in_array($field_item->getName(), $context['included_fields']);
+      });

Here is a naive question: getNonInternal ... is keyed by $name already. Isn't that the same as $field_item->getName() and as such you could array_diff_key it?

amateescu’s picture

Then we could just expand \Drupal\Tests\rest\Functional\EntityResource\EntityTest\EntityTestResourceTestBase, instead of adding lots of new test classes. I like it.

Yay! The huge number of new test classes was exactly the problem :)

  1. +++ b/core/lib/Drupal/Core/TypedData/DataDefinition.php
    @@ -352,4 +352,29 @@ public function __sleep() {
    +   * @return static
    +   *   The object itself for chaining.
    

    This should be only @return $this, without any description.

  2. +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/InternalPropertyTestFieldItem.php
    @@ -0,0 +1,45 @@
    +      ->setComputed(TRUE)
    +      ->setClass(ComputedStringData::class)
    ...
    +      ->setComputed(TRUE)
    +      ->setClass(ComputedStringData::class);
    

    What I meant in #109.4 is that the setComputed() call here doesn't actually do anything.

    The value returned by these two properties is solely determined by the fact that they are using a special class, ComputedStringData.

    In other words, ComputedStringData and ComputedString are not doing anything special based on their 'computed' definition flag being TRUE or FALSE.

dagmar’s picture

Added the first attempt to document what I metioned in #120

Wim Leers’s picture

#123:

  1. +++ b/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php
    @@ -72,22 +77,14 @@ public function normalize($entity, $format = NULL, array $context = []) {
    +      $field_items = array_filter($field_items, function (FieldItemListInterface $field_item) use ($context) {
    +        return in_array($field_item->getName(), $context['included_fields']);
    

    Can't this use array_intersect()?

  2. +++ b/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php
    @@ -72,22 +77,14 @@ public function normalize($entity, $format = NULL, array $context = []) {
    -      // Continue if the current user does not have access to view this field.
    -      if (!$field->access('view', $context['account'])) {
    -        continue;
    -      }
    
    +++ b/core/modules/serialization/src/Normalizer/ComplexDataPropertiesNormalizerTrait.php
    @@ -42,12 +39,17 @@ protected function normalizeProperties(ComplexDataInterface $data, $format, arra
    -  protected static function getNonInternalProperties(ComplexDataInterface $data) {
    -    return array_filter($data->getProperties(TRUE), function (TypedDataInterface $property) {
    +  public static function getNonInternalAccessibleProperties(ComplexDataInterface $data, array $context) {
    +    return array_filter($data->getProperties(TRUE), function (TypedDataInterface $property) use ($context) {
    +      if ($property instanceof AccessibleInterface && !$property->access('view', $context['account'])) {
    

    AFAICT we only had to make this change because we removed access checking (with the continue statement) from the normalizer.

    But was that even necessary?

    AFAICT the #123 interdiff could've been much smaller if the $context['included_fields'] logic was indeed intersected with the list of non-internal fields… but then the access check with the continue statement could've been kept. Which would've meant no changes to the trait, and far fewer changes to the normalizer?


#124:

Looking at how to mock EntityAdapter I was 😦. Then I found \Drupal\Core\Entity\Entity::getTypedData and I was like 😅.

Hahahaha — wonderful use of emojis :D


#126: hah, you made the same remark as I did — I just suggested array_intersect(). Whichever one we end up using — I agree that this can be simplified by using one of them.


#127: Thanks so much for your continued reviews of this issue — it makes a world of difference to have an Entity/Field API maintainer actively looking at this! ❤️❤️❤️

Yay! The huge number of new test classes was exactly the problem :)

Yep — good call! 👍

In other words, ComputedStringData and ComputedString are not doing anything special based on their 'computed' definition flag being TRUE or FALSE.

You're right that in the current patch they don't. But in #2910211: Allow computed exposed properties in ComplexData to support cacheability., they will. The need to bubble cacheability metadata from (computed) properties that are being normalized so the final HTTP response can have the necessary associated cacheability metadata was descoped from this issue in #78 and #86 (and so until those comments/patches, those classes did have a clearer reason to exist).

@tedbow, can you roll a patch for #2910211: Allow computed exposed properties in ComplexData to support cacheability. that layers that cacheability bubbling on top of this patch, so that it's once again clear why we are adding those ComputedString(Data) classes?


#128: 👌 But two nits: s/procces/process/ and s/calculated/computed


Finally, noticed something else:

+++ b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
@@ -87,13 +90,7 @@ protected function constructValue($data, $context) {
+    $denormalized = $this->normalizeProperties($field_item, $format, $context);

🙃😵 — created #2916025: Rename $denormalized to $normalized in \Drupal\hal\Normalizer\FieldItemNormalizer::normalizedFieldValues() to fix this.

Wim Leers’s picture

Wim Leers’s picture

Issue summary: View changes

I also think it's about time that we update the issue summary based on the slightly revised direction since meeting with Entity/Field/Typed Data API maintainers at DrupalCon Vienna two weeks ago.

Wim Leers’s picture

Priority: Normal » Major

Also, how was this Normal? It's Major at least.

tedbow’s picture

#127 @amateescu thanks for review!
1. Fixed
2. what @Wim Leers said in #129

#128 @dagmar thanks for getting this started.

+++ b/core/lib/Drupal/Core/TypedData/DataDefinitionInterface.php
@@ -221,6 +221,10 @@ public function addConstraint($constraint_name, $options = NULL);
+   * Internal properties will not be returned during the normalization procces.
+   * By default, calculated properties will be internal unless otherwise

Not sure if we should mention "normalization" here because that happens in the Serialization module and this changes in the TypeData system. But not sure how to better explain "internal" in a general way.

Also we should probably use "computed" instead of calculated.

#129
1. array_intersect_key instead because the values in $field_items are not the string field names.
I guess this addresses @dawehner's comment in #126
2. Yes we could leave the access check inside \Drupal\hal\Normalizer\ContentEntityNormalizer::normalize but why leave it there when the same logic is needed in \Drupal\serialization\Normalizer\ContentEntityNormalizer::normalize and we can do it in one place.
It makes the interdiff bigger but the simpler overall.

tedbow’s picture

I have started the patch in #2910211: Allow computed exposed properties in ComplexData to support cacheability. which was split off this issue in #78. It seemed simpler not to handle caching in this issue but we still want to be sure our current approach works with caching.

Wim Leers’s picture

but why leave it there when the same logic is needed in \Drupal\serialization\Normalizer\ContentEntityNormalizer::normalize and we can do it in one place.

Because now the trait is doing much more than just normalizing complex data properties. It's no longer about \Drupal\Core\TypedData\ComplexDataInterface like its name indicates, but it's also about \Drupal\Core\Access\AccessibleInterface.

It's also refactoring things that are out of scope to refactor here. If we want to share more code, then perhaps it's better to have this access checking logic only in \Drupal\serialization\Normalizer\ContentEntityNormalizer and make \Drupal\hal\Normalizer\ContentEntityNormalizer layer on top of that. Which would also be out of scope here. But it might make things even simpler. Which is my point exactly: by making this change, we open another discussion, about how to simplfy/share the access-checking logic in two different normalizers. That's not what this issue is about.

In other words: it's about keeping this patch as simple, focused, in-scope as possible.

#134: yay!

tedbow’s picture

#135 re

In other words: it's about keeping this patch as simple, focused, in-scope as possible.

Ok so keeping that in mind. I removed ComplexDataPropertiesNormalizerTrait altogether.

We were using it in 4 normalizers but basically each really on needed to know which of the properties on the ComplexDataInterface object(or entity which can be converted) that are internal.

doing much more than just normalizing complex data properties. It's no longer about \Drupal\Core\TypedData\ComplexDataInterface like its name indicates, but it's also about \Drupal\Core\Access\AccessibleInterface.

Yes that has been in the trait for awhile now so let's remove it. Instead of the trait I added TypedDataInternalPropertiesHelper which has getNonInternalProperties().

So now each of the four normalizers are has more code but is changed much less. So these keeps the patch more focused. Also helper class has no logic around AccessibleInterface which the trait did.

amateescu’s picture

FileSize
4.84 KB

Hmm, maybe I couldn't express very well my point from #109.4 and #127.2, so here it is in code form :)

Basically, a computed property doesn't need a specialized data type, it can just use the string one and have a custom class defined, just like TestProcessed which will be changed in #2910211: Allow computed exposed properties in ComplexData to support cacheability.. And I don't think we need another level of indirection for concatenating two strings, we can just do it in the main "computed string" class.

tedbow’s picture

Status: Needs review » Needs work

The last submitted patch, 138: 2871591-138.patch, failed testing. View results

tedbow’s picture

Status: Needs work » Needs review
FileSize
32.67 KB
4.86 KB

In #138 somehow by shell script to make patch got messed up because of a need to manualing fix the rebase.

Wim Leers’s picture

Status: Needs review » Needs work

#137: oh wow, I see @tedbow says in #138 he was getting that, but I definitely wasn't — thanks for clarifying!


Reviewed the entire patch again:

  1. +++ b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php
    @@ -855,4 +855,12 @@ public function __clone() {
    +  public function isInternal() {
    
    +++ b/core/lib/Drupal/Core/Field/FieldConfigBase.php
    @@ -591,4 +591,15 @@ public function addPropertyConstraints($name, array $constraints) {
    +  public function isInternal() {
    
    +++ b/core/lib/Drupal/Core/TypedData/DataDefinition.php
    @@ -352,4 +352,28 @@ public function __sleep() {
    +  public function isInternal() {
    ...
    +  public function setInternal($internal) {
    
    +++ b/core/lib/Drupal/Core/TypedData/DataDefinitionInterface.php
    @@ -218,4 +218,16 @@ public function getConstraint($constraint_name);
    +  public function isInternal();
    
    +++ b/core/lib/Drupal/Core/TypedData/TypedDataInternalPropertiesHelper.php
    @@ -0,0 +1,27 @@
    +class TypedDataInternalPropertiesHelper {
    
    +++ b/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php
    @@ -72,15 +73,10 @@ public function normalize($entity, $format = NULL, array $context = []) {
    +    $field_items = TypedDataInternalPropertiesHelper::getNonInternalProperties($entity->getTypedData(), $context);
    
    +++ b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
    @@ -91,7 +92,7 @@ protected function normalizedFieldValues(FieldItemInterface $field_item, $format
    -    foreach ($field_item as $property_name => $property) {
    +    foreach (TypedDataInternalPropertiesHelper::getNonInternalProperties($field_item, $context) as $property_name => $property) {
    

    ❤️

    These are all the key changes. And I think they're now A) crystal clear, B) minimal (i.e. zero out of-scope changes). 👍

    I think that makes this an order of magnitude easier to review — hopefully subsystem maintainers & committers agree with that :)

  2. +++ b/core/modules/hal/tests/src/Functional/EntityResource/EntityTest/EntityTestHalJsonInternalPropertyNormalizerTest.php
    @@ -0,0 +1,86 @@
    + * Test that internal properties are not exposed in REST.
    

    (Accuracy) Nit: s/in REST/in the 'hal_json' format/

  3. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestJsonInternalPropertyNormalizerTest.php
    @@ -0,0 +1,87 @@
    + * Test that internal properties are not exposed in REST.
    

    (Accuracy) Nit: s/in REST/in the 'json' format/

  4. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php
    @@ -53,9 +53,19 @@ protected function setUpAuthorization($method) {
    +    $this->container->get('state')->set('entity_test.test_internal', TRUE);
    

    (Clarity) Let's rename this to entity_test.internal_field

    That'd make it clear that this is about testing an internal field. The EntityTest(Hal)JsonInternalPropertyNormalizerTest classes are about testing internal properties.

  5. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php
    @@ -53,9 +53,19 @@ protected function setUpAuthorization($method) {
    +      // returned in normalization. @see \entity_test_entity_base_field_info().
    

    Nit: the @see belongs on the next line.

  6. +++ b/core/modules/serialization/src/Normalizer/ContentEntityNormalizer.php
    @@ -1,6 +1,7 @@
    +use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper;
    

    Supernit: needs blank line above.

  7. +++ b/core/modules/serialization/src/Normalizer/TypedDataNormalizer.php
    @@ -18,7 +20,11 @@ class TypedDataNormalizer extends NormalizerBase {
    -    return $object->getValue();
    +    $value = $object->getValue();
    +    if (is_object($value) && $object instanceof PrimitiveInterface) {
    +      return $object->getCastedValue();
    +    }
    +    return $value;
    

    ⁉️ I still think this should not be necessary.

  8. +++ b/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php
    @@ -77,8 +79,18 @@ public function testNormalize() {
    +    $non_internal_property = $this->getTypedDataProperty(FALSE);
    

    (Clarity) Nit: $value_property

  9. +++ b/core/modules/system/tests/modules/entity_test/config/schema/entity_test.data_types.schema.yml
    @@ -0,0 +1,5 @@
    +# Schema for the configuration of the internal property field type.
    

    (Clarity) Nit: s/internal property/'internal property test'/

  10. +++ b/core/modules/system/tests/modules/entity_test/entity_test.module
    @@ -109,6 +109,11 @@ function entity_test_entity_type_alter(array &$entity_types) {
    +      ->setLabel('Internal')
    

    (Clarity) Nit: s/Internal/Internal field/

  11. +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/InternalPropertyTestFieldItem.php
    @@ -0,0 +1,45 @@
    +      ->setLabel(new TranslatableMarkup('Computed string, non-internal'))
    

    (Clarity) Nit: s/non-internal/non-internal property/

  12. +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/InternalPropertyTestFieldItem.php
    @@ -0,0 +1,45 @@
    +      ->setLabel(new TranslatableMarkup('Computed string, internal'))
    

    (Clarity) Nit: s/internal/internal property/

tedbow’s picture

Status: Needs work » Needs review
FileSize
8.19 KB
31.86 KB

@Wim Leers thanks for the review
1. agreed
2. fixed
3. fixed
4. fixed
5. Fixed
6. super fixed
7. Removed. Yes this may be left over from when the patch had cache tags logic. I removed it locally and ran few tests that involved and it was fine.
8. fixed
9. fixed
10. fixed
11. fixed
12. fixed

Wim Leers’s picture

There are only two things holding me back from RTBC'ing now:

  1. No change record yet.
  2. I am no subsystem maintainer — it should be for example amateescu or fago who RTBCs this patch.

/me is rooting furiously that amateescu will RTBC this :)

amateescu’s picture

Status: Needs review » Needs work

Totally agreed with #141.1 :D

I went through the entire patch as well, and I only found these small problems:

  1. +++ b/core/lib/Drupal/Core/TypedData/DataDefinitionInterface.php
    @@ -218,4 +218,16 @@ public function getConstraint($constraint_name);
    +   * Internal properties will not be returned during the normalization procces.
    

    I don't think we should mention the normalization process directly like this in the generic data definition interface. Maybe we could rephrase to something like:

    "This can be used in a scenario when it is not desirable to expose this data value to an external system."

    Or something along that line :)

  2. +++ b/core/lib/Drupal/Core/TypedData/DataDefinitionInterface.php
    @@ -218,4 +218,16 @@ public function getConstraint($constraint_name);
    +   * By default, calculated properties will be internal unless otherwise
    

    'calculated' -> 'computed' ;)

  3. +++ b/core/lib/Drupal/Core/TypedData/TypedDataInternalPropertiesHelper.php
    @@ -0,0 +1,27 @@
    +class TypedDataInternalPropertiesHelper {
    ...
    +  public static function getNonInternalProperties(ComplexDataInterface $data, array $context) {
    

    This doesn't seem to be really useful as a separate class, I think it would be better if we just put the getNonInternalProperties() inside \Drupal\serialization\Normalizer\ComplexDataNormalizer because most (all?) the other places that use it extend the complex data normalizer class.

    And if there aren't any usages from outside the class hierarchy of ComplexDataNormalizer, it would probably makes sense for it to not be static anymore.

  4. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php
    @@ -53,9 +53,20 @@ protected function setUpAuthorization($method) {
    +      // @see \entity_test_entity_base_field_info().
    

    No need for the leading '\' here, we don't namespace procedural code :)

tedbow’s picture

Status: Needs work » Needs review
FileSize
2.04 KB
31.8 KB

@amateescu thanks for the review!
1. Ok using your suggested wording.
2.

* By default, calculated properties will be internal unless otherwise
* specified.

Removing this altogether because this just the behavior of \Drupal\Core\TypedData\DataDefinition::isInternal()
But \Drupal\Core\Field\BaseFieldDefinition::isInternal() which doesn't extend DataDefinition doesn't do this.
3. Actually there are 4 uses and 1 is \Drupal\serialization\Normalizer\ComplexDataNormalizer and 1 extends it. The other 2 in the HAL module don't extend it. So not most. So not removing TypedDataInternalPropertiesHelper unless you still think it should be in ComplexDataNormalizer
4. Fixed.

#143.1 will start on the change record now.

tedbow’s picture

Issue tags: -Needs change record

Change record: https://www.drupal.org/node/2916592
Could use extra eyes on it.

amateescu’s picture

@tedbow, about #145.3: I would simply put that method in the two classes from hal that use it. I don't know.. maybe it's just me, but providing a trait with a single method which contains a simple array filter is just not worth it :)

Fixed a few things in the change record: https://www.drupal.org/node/2916592/revisions/view/10679389/10679402

tedbow’s picture

  1. I don't know.. maybe it's just me, but providing a trait with a single method which contains a simple array filter is just not worth it :)

    Well to be precise now it is not a trait it is helper class.

    But not only does it contain an array_filter it also calls $data->getProperties(TRUE) to ensure that we bring back computed properties which $data->getProperties() would not.

    Also this is code that would immediately need to be duplicated in contrib in JSON API and Schemata module. JSON API is at least on API First roadmap for getting into core.

    So changing that right now to get your feedback.

    but I did find this problem.

    +++ b/core/lib/Drupal/Core/TypedData/TypedDataInternalPropertiesHelper.php
    @@ -0,0 +1,27 @@
    +   * @param array $context
    +   *   The context passed into the Normalizer.
    +   *
    +   * @return \Drupal\Core\TypedData\TypedDataInterface[]
    +   *   The non-internal properties, keyed by property name.
    +   */
    +  public static function getNonInternalProperties(ComplexDataInterface $data, array $context) {
    

    We don't actually use $context here. Removing

  2. +++ b/core/modules/serialization/tests/src/Unit/Normalizer/ContentEntityNormalizerTest.php
    @@ -101,6 +103,7 @@ public function testNormalizeWithAccountContext() {
    +      // @todo Add test internal field.
    

    Never completed this @todo. Adding but in testNormalize()

mradcliffe’s picture

I reviewed the change record, and it makes sense. I was able to understand what the change was, why it was made in this way (to keep BC), and how to use it. Nicely done @amateescu and @tedbow for writing it.

Should the TypedDataInternalPropertiesHelper class be marked as @internal? I could see its usage, but it's really basic so I don't think it's that big of a loss if it's an internal class.

Wim Leers’s picture

Nit: $this->createMockFieldListItem() should not have default values for its parameters: it’s better to be explicit in tests.

tedbow’s picture

Ok I got rid the defaults except for $user_context. If I don't put a default of NULL and type hint for AccountInterface then there is error when you pass in NULL.

If we going to change the function signature it seem better to use a type hint where we can.

tedbow’s picture

re #149 @mradcliffe
Thanks reviewing the change record.

Also for the committer can you make sure @mradcliffe gets credit for this issue. His comment in #36 was key in understanding how contrib modules were already using our normalizers and helped make sure we didn't make a change that broke a bunch of normalizer classes in contrib. Thanks

Should the TypedDataInternalPropertiesHelper class be marked as @internal? I could see its usage, but it's really basic so I don't think it's that big of a loss if it's an internal class.

If others think that is good idea I guess I would be fine with that but I would prefer it to be not internal. It seems useful for others to use.

amateescu’s picture

Status: Needs review » Reviewed & tested by the community

Let's leave it up to core committers to decide if TypedDataInternalPropertiesHelper should be internal or not. I don't really care *that* much about it to hold this up any longer.

Awesome work has been done here, let's get it in!

The last submitted patch, 117: allow_complexdata_in-2871591-117.patch, failed testing. View results

Wim Leers’s picture

🎉👌🤞

damiankloip’s picture

I agree this looks great. I'm totally +1. There are a couple of things left over from my review in #106 that I think got missed in amongst other patches and reviews. They are mostly small things, and some have already been fixed/picked up in subsequent revirews (Like #106.{7-9}).

Leaving as RTBC for now based on that. Thoughts @tedbow @Wim Leers

tedbow’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
797 bytes
33.92 KB

@damiankloip glad you are good with RTBC!

They are mostly small things, and some have already been fixed/picked up in subsequent revirews (Like #106.{7-9}).

So going through all of#106

1. was for \Drupal\Core\TypedData\DataDefinition::setInternal

Just to be pedantic, we could cast to a bool here.

We could but setComputed, setRequired, setReadOnly are not doing this. Is there a pattern we follow in general?
2. We are not longer using InternalDataDefinitionTrait but \Drupal\Core\TypedData\DataDefinition::isInternal() no longer has the else like you are asking for.
3. Meaning it would be better to say "external" in the test? I prefer "non_internal" because it is easier to find when searching the tests for the "internal" string.
4. The field will not already be there. We can't create the field in setUp() because createEntity() is called in parent::setup() and we need the field created before we create the entity. But I think createEntity() can be called more than once test so we have to have if statement to make sure we don't try to create it more than once.
5. see 3)
6. Honestly I didn't know you could add multiple traits this way. fixed.

7.

This is where I mean access would now not be checked on the property level, compared to the normalizeProperties methods from the new trait.

Because we are not using \Drupal\Core\TypedData\TypedDataInternalPropertiesHelper::getNonInternalProperties this is where I mean access would now not be checked on the property level, compared to the normalizeProperties methods from the new trait and we don't the trait we are not changing anything anymore about on which the access is being checked at.

When we were using the trait I now see that introduced checking access on property level. This kind of change would have been out of scope for this issue.
8. We are not longer doing this.
9. We are no longer changing TypedDataNormalizer in the latest patch.

So only 1 change for 6) and just a use statement.

damiankloip’s picture

Great - thanks for the response @tedbow. As I mentioned in my previous comment, most of it was already fixed in one way or another, which is nice. All the major things were picked up or implicitly fixed by changes to the direction of the patch, so ++++

RE point 3; I mean that the 'non_internal_value' key has a string of 'Computed! value to be internal'. Should this be 'Computed! value to be non internal' or 'Computed! value to be exposed' ?

Wim Leers’s picture

Status: Needs review » Needs work

#158.3: hah, nice nit!

tedbow’s picture

Status: Needs work » Needs review
FileSize
3.93 KB
34.03 KB

@damiankloip duh, I totally missed that. nice catch.

Changed value to be

This value shall not be internal!

Wim Leers’s picture

Status: Needs review » Reviewed & tested by the community

😀

👍

Back to RTBC per #153!

damiankloip’s picture

Haha, nice. Thanks @tedbow!

Agree with the RTBC 100% now 👍

larowlan’s picture

Adding review credits

larowlan’s picture

Status: Reviewed & tested by the community » Fixed

Committed as c74c39e and pushed to 8.5.x.

Published change record.

Wim Leers’s picture

🎉🙃🍾 After a year of discussing, and nearly half a year of discussing it in this thread, working on a patch, and building towards consensus, this landed! Hurray!

That means #2910211: Allow computed exposed properties in ComplexData to support cacheability. is now unblocked (which was split off from this issue in #86, to make the scope narrower/clearer here). That's now the next blocker, and then we'll finally be able to land #2825487: Fix normalization of File entities: file entities should expose the file URL as a computed property on the 'uri' base field, #2825812: ImageItem should have an "derivatives" computed property, to expose all image style URLs, #2626924: Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata and #2577923: MenuLinkContent entities pointing to nodes are not deployable: LinkItem should have a "target_uuid" computed property!

It also means that the https://www.drupal.org/project/jsonapi_extras module no longer needs to have its own mechanism for "disabling" certain fields: they can now just be marked internal. GraphQL would need to do the same. That'd mean that entities exposed via Serialization + REST + JSON API + GraphQL would all be exposed consistently. Created #2921257: On Drupal 8.5, JSON API should respect Typed Data's new ::isInternal(), and JSON API Extras should use ::setInternal() for that. It'll be tricky there, because this new API is only available in 8.5, which is not yet out.
If the core committers would consider committing this to 8.4 too, that'd allow the ecosystem to move along 6 months faster, which would lead to a more mature API-First Drupal considerably faster…

xjm’s picture

Thanks @Wim Leers! @larowlan and I discussed this and we don't think it's really a candidate for backport at this time. Let's focus on making sure all the followups land in 8.5.x and then see where we're at. (Keep in mind that 8.5.0 is less than 4 months away and the alpha deadline is a mere 10 weeks away.)

xjm’s picture

The added test from this issue EntityTestHalJsonInternalPropertyNormalizerTest seems to be failing nightly on PHP 5.5 & SQLite 3.8 (of all things)? https://www.drupal.org/pift-ci-job/804598

Edit: Also postgres and 5.5: https://www.drupal.org/pift-ci-job/804596

I haven't reverted yet because the main environments aren't failing; maybe the version of 5.5 on the other bots is different. We already have other issues with 5.5.9. But let's confirm what's going on to avoid a revert.

Anonymous’s picture

@tacituseu recognized this error like #2919863: Discover why gc_disable() during non-interactive install improves tests stability. I also tested his patch and it works perfect. See #2879048-171: Ignore: patch testing issue for #2919863.


Last time we faced with this error after #2849674: Complex job in ViewExecutable::unserialize() causes data corruption. Looks like issues with 'Complex' word in the title are dangerous :P
Wim Leers’s picture

#166: thanks for looking into backportability, much appreciate it. There aren't any follow-ups, HEAD is in a good state with this patch being committed. There are lots of super important bugfixes that were blocked on this, and those we will definitely continue working on! But we need one more issue to land for those to be able to move forward: #2910211: Allow computed exposed properties in ComplexData to support cacheability..

#167: 😨😡 It's interesting that those failures are only happening on PHP 5.5 (and on those particular DBs). But … per http://php.net/supported-versions.php, 5.5 is not supported at all, it doesn't even have security support. 5.6 doesn't have this problem. So, I don't know how we're ever going to get to the bottom of this? :(
#168: thanks for those links, for your research, and for that funny association :D

Okay, so @vaplas started to actively debug this in #2879048-169: Ignore: patch testing issue for #2919863 and later. I'm now following that issue and will help out.

  • larowlan committed c74c39e on 8.5.x
    Issue #2871591 by tedbow, Wim Leers, dagmar, borisson_, amateescu,...
Wim Leers’s picture

Status: Fixed » Closed (fixed)

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