diff --git a/core/modules/jsonapi/jsonapi.services.yml b/core/modules/jsonapi/jsonapi.services.yml index 16c7f4dc95..21f322d0ff 100644 --- a/core/modules/jsonapi/jsonapi.services.yml +++ b/core/modules/jsonapi/jsonapi.services.yml @@ -47,7 +47,10 @@ services: tags: - { name: jsonapi_normalizer } serializer.normalizer.content_entity.jsonapi: - class: Drupal\jsonapi\Normalizer\ContentEntityDenormalizer + alias: serializer.normalizer.fieldable_entity.jsonapi + deprecated: The "%alias_id%" service is deprecated. You should use the 'serializer.normalizer.fieldable_entity.jsonapi' service instead. + serializer.normalizer.fieldable_entity.jsonapi: + class: Drupal\jsonapi\Normalizer\FieldableEntityDenormalizer arguments: ['@entity_type.manager', '@entity_field.manager', '@plugin.manager.field.field_type'] tags: - { name: jsonapi_normalizer } diff --git a/core/modules/jsonapi/src/Controller/EntityResource.php b/core/modules/jsonapi/src/Controller/EntityResource.php index 5c43d3ee24..ae169b46f0 100644 --- a/core/modules/jsonapi/src/Controller/EntityResource.php +++ b/core/modules/jsonapi/src/Controller/EntityResource.php @@ -7,7 +7,6 @@ use Drupal\Component\Serialization\Json; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Config\Entity\ConfigEntityInterface; -use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityRepositoryInterface; @@ -1048,8 +1047,9 @@ protected function respondWithCollection(ResourceObjectData $primary_data, Data * types. */ protected function updateEntityField(ResourceType $resource_type, EntityInterface $origin, EntityInterface $destination, $field_name) { - // The update is different for configuration entities and content entities. - if ($origin instanceof ContentEntityInterface && $destination instanceof ContentEntityInterface) { + // The update is different for configuration entities and fieldable + // entities. + if ($origin instanceof FieldableEntityInterface && $destination instanceof FieldableEntityInterface) { // First scenario: both are content entities. $field_name = $resource_type->getInternalName($field_name); $destination_field_list = $destination->get($field_name); diff --git a/core/modules/jsonapi/src/JsonApiResource/ResourceObject.php b/core/modules/jsonapi/src/JsonApiResource/ResourceObject.php index 8a08cb3d4e..a3a4d9ae03 100644 --- a/core/modules/jsonapi/src/JsonApiResource/ResourceObject.php +++ b/core/modules/jsonapi/src/JsonApiResource/ResourceObject.php @@ -8,6 +8,7 @@ use Drupal\Core\Config\Entity\ConfigEntityInterface; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Entity\RevisionableInterface; use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper; use Drupal\Core\Url; @@ -211,10 +212,14 @@ public function toUrl() { * entity, the fields will be scalar values or arrays. */ protected static function extractFieldsFromEntity(ResourceType $resource_type, EntityInterface $entity) { - assert($entity instanceof ContentEntityInterface || $entity instanceof ConfigEntityInterface); - return $entity instanceof ContentEntityInterface - ? static::extractContentEntityFields($resource_type, $entity) - : static::extractConfigEntityFields($resource_type, $entity); + if ($entity instanceof FieldableEntityInterface) { + return static::extractFieldableEntityFields($resource_type, $entity); + } + elseif ($entity instanceof ConfigEntityInterface) { + return static::extractConfigEntityFields($resource_type, $entity); + } + + return []; } /** @@ -267,12 +272,32 @@ protected static function buildLinksFromEntity(ResourceType $resource_type, Enti * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type * The JSON:API resource type of the given entity. * @param \Drupal\Core\Entity\ContentEntityInterface $entity - * The config entity from which fields should be extracted. + * The content entity from which fields should be extracted. * * @return \Drupal\Core\Field\FieldItemListInterface[] * The fields extracted from a content entity. + * + * @deprecated in Drupal 8.8.x and will be removed before Drupal 9.0.0. + * Use \Drupal\jsonapi\JsonApiResource\ResourceObject::extractFieldableEntityFields() + * instead. */ protected static function extractContentEntityFields(ResourceType $resource_type, ContentEntityInterface $entity) { + @trigger_error('\Drupal\jsonapi\JsonApiResource\ResourceObject::extractContentEntityFields() has been deprecated in favor of \Drupal\jsonapi\JsonApiResource\ResourceObject::extractFieldableEntityFields(). Use that instead.'); + return static::extractFieldableEntityFields($resource_type, $entity); + } + + /** + * Extracts a fieldable entity's fields. + * + * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type + * The JSON:API resource type of the given entity. + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity + * The fieldable entity from which fields should be extracted. + * + * @return \Drupal\Core\Field\FieldItemListInterface[] + * The fields extracted from a content entity. + */ + protected static function extractFieldableEntityFields(ResourceType $resource_type, FieldableEntityInterface $entity) { $output = []; $fields = TypedDataInternalPropertiesHelper::getNonInternalProperties($entity->getTypedData()); // Filter the array based on the field names. diff --git a/core/modules/jsonapi/src/Normalizer/ContentEntityDenormalizer.php b/core/modules/jsonapi/src/Normalizer/ContentEntityDenormalizer.php index cd821d4975..19e795a19f 100644 --- a/core/modules/jsonapi/src/Normalizer/ContentEntityDenormalizer.php +++ b/core/modules/jsonapi/src/Normalizer/ContentEntityDenormalizer.php @@ -2,6 +2,8 @@ namespace Drupal\jsonapi\Normalizer; +@trigger_error('\Drupal\jsonapi\Normalizer\ContentEntityDenormalizer has been deprecated in favor of \Drupal\jsonapi\Normalizer\FieldableEntityDenormalizer. Use that instead.'); + use Drupal\Core\Entity\ContentEntityInterface; use Drupal\jsonapi\ResourceType\ResourceType; use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; diff --git a/core/modules/jsonapi/src/Normalizer/FieldableEntityDenormalizer.php b/core/modules/jsonapi/src/Normalizer/FieldableEntityDenormalizer.php new file mode 100644 index 0000000000..ec738cb9c7 --- /dev/null +++ b/core/modules/jsonapi/src/Normalizer/FieldableEntityDenormalizer.php @@ -0,0 +1,83 @@ +fieldManager->getFieldMap()[$resource_type->getEntityTypeId()]; + + $entity_type_id = $resource_type->getEntityTypeId(); + $entity_type_definition = $this->entityTypeManager->getDefinition($entity_type_id); + $bundle_key = $entity_type_definition->getKey('bundle'); + $uuid_key = $entity_type_definition->getKey('uuid'); + + // Translate the public fields into the entity fields. + foreach ($data as $public_field_name => $field_value) { + $internal_name = $resource_type->getInternalName($public_field_name); + + // Skip any disabled field, except the always required bundle key and + // required-in-case-of-PATCHing uuid key. + // @see \Drupal\jsonapi\ResourceType\ResourceTypeRepository::getFieldMapping() + if ($resource_type->hasField($internal_name) && !$resource_type->isFieldEnabled($internal_name) && $bundle_key !== $internal_name && $uuid_key !== $internal_name) { + continue; + } + + if (!isset($field_map[$internal_name]) || !in_array($resource_type->getBundle(), $field_map[$internal_name]['bundles'], TRUE)) { + throw new UnprocessableEntityHttpException(sprintf( + 'The attribute %s does not exist on the %s resource type.', + $internal_name, + $resource_type->getTypeName() + )); + } + + $field_type = $field_map[$internal_name]['type']; + $field_class = $this->pluginManager->getDefinition($field_type)['list_class']; + + $field_denormalization_context = array_merge($context, [ + 'field_type' => $field_type, + 'field_name' => $internal_name, + 'field_definition' => $this->fieldManager->getFieldDefinitions($resource_type->getEntityTypeId(), $resource_type->getBundle())[$internal_name], + ]); + $data_internal[$internal_name] = $this->serializer->denormalize($field_value, $field_class, $format, $field_denormalization_context); + } + + return $data_internal; + } + +} diff --git a/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php b/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php index 995dfeed42..38b6b6b5bb 100644 --- a/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php +++ b/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php @@ -5,9 +5,9 @@ use Drupal\Component\Assertion\Inspector; use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Config\Entity\ConfigEntityInterface; use Drupal\Core\Config\Entity\ConfigEntityTypeInterface; use Drupal\Core\Entity\ContentEntityNullStorage; -use Drupal\Core\Entity\ContentEntityTypeInterface; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Entity\EntityTypeInterface; @@ -195,7 +195,6 @@ public function getByTypeName($type_name) { */ protected static function getFieldMapping(array $field_names, EntityTypeInterface $entity_type, $bundle) { assert(Inspector::assertAllStrings($field_names)); - assert($entity_type instanceof ContentEntityTypeInterface || $entity_type instanceof ConfigEntityTypeInterface); assert(is_string($bundle) && !empty($bundle), 'A bundle ID is required. Bundleless entity types should pass the entity type ID again.'); $mapping = []; @@ -263,14 +262,14 @@ protected static function getFieldMapping(array $field_names, EntityTypeInterfac * All field names. */ protected function getAllFieldNames(EntityTypeInterface $entity_type, $bundle) { - if ($entity_type instanceof ContentEntityTypeInterface) { + if (is_a($entity_type->getClass(), FieldableEntityInterface::class, TRUE)) { $field_definitions = $this->entityFieldManager->getFieldDefinitions( $entity_type->id(), $bundle ); return array_keys($field_definitions); } - elseif ($entity_type instanceof ConfigEntityTypeInterface) { + elseif (is_a($entity_type->getClass(), ConfigEntityInterface::class, TRUE)) { // @todo Uncomment the first line, remove everything else once https://www.drupal.org/project/drupal/issues/2483407 lands. // return array_keys($entity_type->getPropertiesToExport()); $export_properties = $entity_type->getPropertiesToExport(); @@ -281,9 +280,8 @@ protected function getAllFieldNames(EntityTypeInterface $entity_type, $bundle) { return ['id', 'type', 'uuid', '_core']; } } - else { - throw new \LogicException("Only content and config entity types are supported."); - } + + return []; } /**