Commit 1ab09321 authored by plach's avatar plach
Browse files

Issue #2957385 by Wim Leers, gabesullice, jibran, tim.plunkett:...

Issue #2957385 by Wim Leers, gabesullice, jibran, tim.plunkett: FieldItemNormalizer never calls @DataType-level normalizer service' ::denormalize() method
parent 1b46f2f6
......@@ -4,6 +4,7 @@
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper;
use Drupal\serialization\Normalizer\FieldableEntityNormalizerTrait;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
/**
......@@ -11,6 +12,8 @@
*/
class FieldItemNormalizer extends NormalizerBase {
use FieldableEntityNormalizerTrait;
/**
* {@inheritdoc}
*/
......@@ -57,21 +60,6 @@ public function denormalize($data, $class, $format = NULL, array $context = [])
return $field_item;
}
/**
* Build the field item value using the incoming data.
*
* @param $data
* The incoming data for this field item.
* @param $context
* The context passed into the Normalizer.
*
* @return mixed
* The value to use in Entity::setValue().
*/
protected function constructValue($data, $context) {
return $data;
}
/**
* Normalizes field values for an item.
*
......
......@@ -1575,18 +1575,43 @@ protected function assertStoredEntityMatchesSentNormalization(array $sent_normal
// Some top-level keys in the normalization may not be fields on the
// entity (for example '_links' and '_embedded' in the HAL normalization).
if ($modified_entity->hasField($field_name)) {
$field_type = $modified_entity->get($field_name)->getFieldDefinition()->getType();
// Fields are stored in the database, when read they are represented
// as strings in PHP memory. The exception: field types that are
// stored in a serialized way. Hence we need to cast most expected
// field normalizations to strings.
$expected_field_normalization = ($field_type !== 'map')
? static::castToString($field_normalization)
: $field_normalization;
$field_definition = $modified_entity->get($field_name)->getFieldDefinition();
$property_definitions = $field_definition->getItemDefinition()->getPropertyDefinitions();
$expected_stored_data = [];
// Some fields don't have any property definitions, so there's nothing
// to denormalize.
if (empty($property_definitions)) {
$expected_stored_data = $field_normalization;
}
else {
// Denormalize every sent field item property to make it possible to
// compare against the stored value.
$denormalization_context = ['field_definition' => $field_definition];
foreach ($field_normalization as $delta => $expected_field_item_normalization) {
foreach ($property_definitions as $property_name => $property_definition) {
// Not every property is required to be sent.
if (!array_key_exists($property_name, $field_normalization[$delta])) {
continue;
}
// Computed properties are not stored.
if ($property_definition->isComputed()) {
continue;
}
$property_value = $field_normalization[$delta][$property_name];
$property_value_class = $property_definitions[$property_name]->getClass();
$expected_stored_data[$delta][$property_name] = $this->serializer->supportsDenormalization($property_value, $property_value_class, NULL, $denormalization_context)
? $this->serializer->denormalize($property_value, $property_value_class, NULL, $denormalization_context)
: $property_value;
}
}
// Fields are stored in the database, when read they are represented
// as strings in PHP memory.
$expected_stored_data = static::castToString($expected_stored_data);
}
// Subset, not same, because we can e.g. send just the target_id for the
// bundle in a PATCH or POST request; the response will include more
// properties.
$this->assertArraySubset($expected_field_normalization, $modified_entity->get($field_name)->getValue(), TRUE);
$this->assertArraySubset($expected_stored_data, $modified_entity->get($field_name)->getValue(), TRUE);
}
}
}
......
......@@ -11,6 +11,8 @@
*/
class FieldItemNormalizer extends ComplexDataNormalizer implements DenormalizerInterface {
use FieldableEntityNormalizerTrait;
/**
* {@inheritdoc}
*/
......@@ -35,23 +37,4 @@ public function denormalize($data, $class, $format = NULL, array $context = [])
return $field_item;
}
/**
* Build the field item value using the incoming data.
*
* Most normalizers that extend this class can simply use this method to
* construct the denormalized value without having to override denormalize()
* and reimplementing its validation logic or its call to set the field value.
*
* @param mixed $data
* The incoming data for this field item.
* @param array $context
* The context passed into the Normalizer.
*
* @return mixed
* The value to use in Entity::setValue().
*/
protected function constructValue($data, $context) {
return $data;
}
}
......@@ -4,6 +4,8 @@
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\TypedData\FieldItemDataDefinitionInterface;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
/**
......@@ -193,4 +195,67 @@ protected function getEntityTypeManager() {
return $this->entityTypeManager;
}
/**
* Build the field item value using the incoming data.
*
* Most normalizers that extend this class can simply use this method to
* construct the denormalized value without having to override denormalize()
* and reimplementing its validation logic or its call to set the field value.
*
* It's recommended to not override this and instead provide a (de)normalizer
* at the DataType level.
*
* @param mixed $data
* The incoming data for this field item.
* @param array $context
* The context passed into the Normalizer.
*
* @return mixed
* The value to use in Entity::setValue().
*/
protected function constructValue($data, $context) {
$field_item = $context['target_instance'];
// Get the property definitions.
assert($field_item instanceof FieldItemInterface);
$field_definition = $field_item->getFieldDefinition();
$item_definition = $field_definition->getItemDefinition();
assert($item_definition instanceof FieldItemDataDefinitionInterface);
$property_definitions = $item_definition->getPropertyDefinitions();
if (!is_array($data)) {
$property_value = $data;
$property_value_class = $property_definitions[$item_definition->getMainPropertyName()]->getClass();
if ($this->serializer->supportsDenormalization($property_value, $property_value_class, NULL, $context)) {
return $this->serializer->denormalize($property_value, $property_value_class, NULL, $context);
}
else {
return $property_value;
}
}
$data_internal = [];
if (!empty($property_definitions)) {
foreach ($property_definitions as $property_name => $property_definition) {
// Not every property is required to be sent.
if (!array_key_exists($property_name, $data)) {
continue;
}
$property_value = $data[$property_name];
$property_value_class = $property_definition->getClass();
if ($this->serializer->supportsDenormalization($property_value, $property_value_class, NULL, $context)) {
$data_internal[$property_name] = $this->serializer->denormalize($property_value, $property_value_class, NULL, $context);
}
else {
$data_internal[$property_name] = $property_value;
}
}
}
else {
$data_internal = $data;
}
return $data_internal;
}
}
<?php
namespace Drupal\test_datatype_boolean_emoji_normalizer\Normalizer;
use Drupal\Core\TypedData\Plugin\DataType\BooleanData;
use Drupal\serialization\Normalizer\NormalizerBase;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
/**
* Normalizes boolean data weirdly: renders them as 👍 (TRUE) or 👎 (FALSE).
*/
class BooleanNormalizer extends NormalizerBase implements DenormalizerInterface {
/**
* {@inheritdoc}
*/
protected $supportedInterfaceOrClass = BooleanData::class;
/**
* {@inheritdoc}
*/
public function normalize($object, $format = NULL, array $context = []) {
return $object->getValue() ? '👍' : '👎';
}
/**
* {@inheritdoc}
*/
public function denormalize($data, $class, $format = NULL, array $context = []) {
if (!in_array($data, ['👍', '👎'], TRUE)) {
throw new \UnexpectedValueException('Only 👍 and 👎 are acceptable values.');
}
return $data === '👍';
}
}
name: 'Test @DataType normalizer'
type: module
description: 'Provides test support for @DataType-level normalization.'
package: Testing
version: VERSION
core: 8.x
services:
serializer.normalizer.boolean.datatype.emoji:
class: Drupal\test_datatype_boolean_emoji_normalizer\Normalizer\BooleanNormalizer
tags:
# The priority must be higher than serializer.normalizer.primitive_data.
- { name: normalizer , priority: 1000 }
<?php
namespace Drupal\test_fieldtype_boolean_emoji_normalizer\Normalizer;
use Drupal\Core\Field\Plugin\Field\FieldType\BooleanItem;
use Drupal\serialization\Normalizer\FieldItemNormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
/**
* Normalizes boolean fields weirdly: renders them as 👍 (TRUE) or 👎 (FALSE).
*/
class BooleanItemNormalizer extends FieldItemNormalizer implements DenormalizerInterface {
/**
* {@inheritdoc}
*/
protected $supportedInterfaceOrClass = BooleanItem::class;
/**
* {@inheritdoc}
*/
public function normalize($object, $format = NULL, array $context = []) {
$data = parent::normalize($object, $format, $context);
$data['value'] = $data['value'] ? '👍' : '👎';
return $data;
}
/**
* {@inheritdoc}
*/
protected function constructValue($data, $context) {
// Just like \Drupal\serialization\Normalizer\FieldItemNormalizer's logic
// for denormalization, which uses TypedDataInterface::setValue(), allow the
// keying by main property name ("value") to be implied.
if (!is_array($data)) {
$data = ['value' => $data];
}
if (!in_array($data['value'], ['👍', '👎'], TRUE)) {
throw new \UnexpectedValueException('Only 👍 and 👎 are acceptable values.');
}
$data['value'] = ($data['value'] === '👍');
return $data;
}
}
name: 'Test @FieldType normalizer'
type: module
description: 'Provides test support for @FieldType-level normalization.'
package: Testing
version: VERSION
core: 8.x
services:
serializer.normalizer.boolean.fieldtype.emoji:
class: Drupal\test_fieldtype_boolean_emoji_normalizer\Normalizer\BooleanItemNormalizer
tags:
# The priority must be higher than serialization.normalizer.field_item.
- { name: normalizer , priority: 1000 }
......@@ -77,6 +77,19 @@ protected function setUp() {
'weight' => 0,
],
])->save();
FieldStorageConfig::create([
'entity_type' => 'entity_test_mulrev',
'field_name' => 'field_test_boolean',
'type' => 'boolean',
'cardinality' => 1,
'translatable' => FALSE,
])->save();
FieldConfig::create([
'entity_type' => 'entity_test_mulrev',
'field_name' => 'field_test_boolean',
'bundle' => 'entity_test_mulrev',
'label' => 'Test boolean',
])->save();
// Create a test entity to serialize.
$this->values = [
......@@ -85,6 +98,9 @@ protected function setUp() {
'value' => $this->randomMachineName(),
'format' => 'full_html',
],
'field_test_boolean' => [
'value' => FALSE,
],
];
$this->entity = EntityTestMulRev::create($this->values);
$this->entity->save();
......@@ -132,4 +148,96 @@ public function testFieldDenormalizeWithScalarValue() {
$this->serializer->denormalize($normalized, $this->entityClass, 'json');
}
/**
* Tests a format-agnostic normalizer.
*
* @param string[] $test_modules
* The test modules to install.
* @param string $format
* The format to test. (NULL results in the format-agnostic normalization.)
*
* @dataProvider providerTestCustomBooleanNormalization
*/
public function testCustomBooleanNormalization(array $test_modules, $format) {
// Asserts the entity contains the value we set.
$this->assertSame(FALSE, $this->entity->field_test_boolean->value);
// Asserts normalizing the entity using core's 'serializer' service DOES
// yield the value we set.
$core_normalization = $this->container->get('serializer')->normalize($this->entity, $format);
$this->assertSame(FALSE, $core_normalization['field_test_boolean'][0]['value']);
$assert_denormalization = function (array $normalization) use ($format) {
$denormalized_entity = $this->container->get('serializer')->denormalize($normalization, EntityTestMulRev::class, $format, []);
$this->assertInstanceOf(EntityTestMulRev::class, $denormalized_entity);
$this->assertSame(TRUE, $denormalized_entity->field_test_boolean->value);
};
// Asserts denormalizing the entity DOES yield the value we set:
// - when using the detailed representation
$core_normalization['field_test_boolean'][0]['value'] = TRUE;
$assert_denormalization($core_normalization);
// - and when using the shorthand representation
$core_normalization['field_test_boolean'][0] = TRUE;
$assert_denormalization($core_normalization);
// Install test module that contains a high-priority alternative normalizer.
$this->enableModules($test_modules);
// Asserts normalizing the entity DOES NOT ANYMORE yield the value we set.
$core_normalization = $this->container->get('serializer')->normalize($this->entity, $format);
$this->assertSame('👎', $core_normalization['field_test_boolean'][0]['value']);
// Asserts denormalizing the entity DOES NOT ANYMORE yield the value we set:
// - when using the detailed representation
$core_normalization['field_test_boolean'][0]['value'] = '👍';
$assert_denormalization($core_normalization);
// - and when using the shorthand representation
$core_normalization['field_test_boolean'][0] = '👍';
$assert_denormalization($core_normalization);
}
/**
* Data provider.
*
* @return array
* Test cases.
*/
public function providerTestCustomBooleanNormalization() {
return [
'Format-agnostic @FieldType-level normalizers SHOULD be able to affect the format-agnostic normalization' => [
['test_fieldtype_boolean_emoji_normalizer'],
NULL,
],
'Format-agnostic @DataType-level normalizers SHOULD be able to affect the format-agnostic normalization' => [
['test_datatype_boolean_emoji_normalizer'],
NULL,
],
'Format-agnostic @FieldType-level normalizers SHOULD be able to affect the JSON normalization' => [
['test_fieldtype_boolean_emoji_normalizer'],
'json',
],
'Format-agnostic @DataType-level normalizers SHOULD be able to affect the JSON normalization' => [
['test_datatype_boolean_emoji_normalizer'],
'json',
],
'Format-agnostic @FieldType-level normalizers SHOULD be able to affect the HAL+JSON normalization' => [
['test_fieldtype_boolean_emoji_normalizer'],
'hal_json',
],
'Format-agnostic @DataType-level normalizers SHOULD be able to affect the HAL+JSON normalization' => [
['test_datatype_boolean_emoji_normalizer', 'hal'],
'hal_json',
],
'Format-agnostic @FieldType-level normalizers SHOULD be able to affect the XML normalization' => [
['test_fieldtype_boolean_emoji_normalizer'],
'xml',
],
'Format-agnostic @DataType-level normalizers SHOULD be able to affect the XML normalization' => [
['test_datatype_boolean_emoji_normalizer', 'hal'],
'xml',
],
];
}
}
......@@ -4,6 +4,7 @@
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
use Drupal\Core\GeneratedUrl;
use Drupal\Core\TypedData\Type\IntegerInterface;
use Drupal\Core\TypedData\TypedDataInterface;
......@@ -83,6 +84,8 @@ protected function setUp() {
->willReturn(new \ArrayIterator(['target_id' => []]));
$this->fieldDefinition = $this->prophesize(FieldDefinitionInterface::class);
$this->fieldDefinition->getItemDefinition()
->willReturn($this->prophesize(FieldItemDataDefinition::class)->reveal());
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment