From 7dfe3876f5e76e440ec136e1dec02d2f7110de5e Mon Sep 17 00:00:00 2001 From: Lee Rowlands <lee.rowlands@previousnext.com.au> Date: Tue, 1 Oct 2019 11:17:01 +1000 Subject: [PATCH] Issue #3014277 by gabesullice, e0ipso, Wim Leers, larowlan, jibran, ndobromirov, catch: ResourceTypes should know about their fields --- .../jsonapi/src/Context/FieldResolver.php | 73 ++---- .../jsonapi/src/Controller/EntityResource.php | 45 ++-- .../jsonapi/src/Controller/FileUpload.php | 2 +- .../src/JsonApiResource/Relationship.php | 9 +- .../EntityReferenceFieldNormalizer.php | 118 ++++++++++ .../src/Normalizer/FieldNormalizer.php | 14 +- .../jsonapi/src/ResourceType/ResourceType.php | 208 ++++++++++++++---- .../ResourceType/ResourceTypeAttribute.php | 16 ++ .../src/ResourceType/ResourceTypeField.php | 129 +++++++++++ .../ResourceType/ResourceTypeRelationship.php | 63 ++++++ .../ResourceType/ResourceTypeRepository.php | 103 ++++++--- .../CountableResourceTypeRepository.php | 3 +- .../AliasingResourceTypeRepository.php | 10 +- .../ResourceTypeRepositoryTest.php | 2 +- .../src/Kernel/Serializer/SerializerTest.php | 14 +- .../ResourceIdentifierNormalizerTest.php | 7 +- .../tests/src/Unit/Routing/RoutesTest.php | 10 +- 17 files changed, 661 insertions(+), 165 deletions(-) create mode 100644 core/modules/jsonapi/src/Normalizer/EntityReferenceFieldNormalizer.php create mode 100644 core/modules/jsonapi/src/ResourceType/ResourceTypeAttribute.php create mode 100644 core/modules/jsonapi/src/ResourceType/ResourceTypeField.php create mode 100644 core/modules/jsonapi/src/ResourceType/ResourceTypeRelationship.php diff --git a/core/modules/jsonapi/src/Context/FieldResolver.php b/core/modules/jsonapi/src/Context/FieldResolver.php index ef58a086ddab..7f65f3d8aa26 100644 --- a/core/modules/jsonapi/src/Context/FieldResolver.php +++ b/core/modules/jsonapi/src/Context/FieldResolver.php @@ -19,6 +19,7 @@ use Drupal\Core\TypedData\DataReferenceDefinitionInterface; use Drupal\Core\TypedData\DataReferenceTargetDefinition; use Drupal\jsonapi\ResourceType\ResourceType; +use Drupal\jsonapi\ResourceType\ResourceTypeRelationship; use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface; use Drupal\Core\Http\Exception\CacheableBadRequestHttpException; @@ -336,7 +337,7 @@ public function resolveInternalEntityQueryPath($resource_type, $external_field_n } // Get all of the referenceable resource types. - $resource_types = $this->getReferenceableResourceTypes($candidate_definitions); + $resource_types = $this->getRelatableResourceTypes($resource_types, $candidate_definitions); $at_least_one_entity_reference_field = FALSE; $candidate_property_names = array_unique(NestedArray::mergeDeepArray(array_map(function (FieldItemDataDefinitionInterface $definition) use (&$at_least_one_entity_reference_field) { @@ -546,54 +547,27 @@ protected function isMemberFilterable($external_name, array $resource_types) { /** * Get the referenceable ResourceTypes for a set of field definitions. * - * @param \Drupal\Core\Field\FieldDefinitionInterface[] $definitions + * @param \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types * The resource types on which the reference field might exist. + * @param \Drupal\Core\Field\TypedData\FieldItemDataDefinitionInterface[] $definitions + * The field item definitions of targeted fields, keyed by the resource + * type name on which they reside. * * @return \Drupal\jsonapi\ResourceType\ResourceType[] * The referenceable target resource types. */ - protected function getReferenceableResourceTypes(array $definitions) { - return array_reduce($definitions, function ($result, $definition) { - $resource_types = array_filter( - $this->collectResourceTypesForReference($definition) - ); - $type_names = array_map(function ($resource_type) { - /* @var \Drupal\jsonapi\ResourceType\ResourceType $resource_type */ - return $resource_type->getTypeName(); - }, $resource_types); - return array_merge($result, array_combine($type_names, $resource_types)); - }, []); - } - - /** - * Build a list of resource types depending on which bundles are referenced. - * - * @param \Drupal\Core\Field\TypedData\FieldItemDataDefinitionInterface $item_definition - * The reference definition. - * - * @return \Drupal\jsonapi\ResourceType\ResourceType[] - * The list of resource types. - */ - protected function collectResourceTypesForReference(FieldItemDataDefinitionInterface $item_definition) { - $main_property_definition = $item_definition->getPropertyDefinition( - $item_definition->getMainPropertyName() - ); - - // Check if the field is a flavor of an Entity Reference field. - if (!$main_property_definition instanceof DataReferenceTargetDefinition) { - return []; + protected function getRelatableResourceTypes(array $resource_types, array $definitions) { + $relatable_resource_types = []; + foreach ($resource_types as $resource_type) { + $definition = $definitions[$resource_type->getTypeName()]; + $resource_type_field = $resource_type->getFieldByInternalName($definition->getFieldDefinition()->getName()); + if ($resource_type_field instanceof ResourceTypeRelationship) { + foreach ($resource_type_field->getRelatableResourceTypes() as $relatable_resource_type) { + $relatable_resource_types[$relatable_resource_type->getTypeName()] = $relatable_resource_type; + } + } } - $entity_type_id = $item_definition->getSetting('target_type'); - $handler_settings = $item_definition->getSetting('handler_settings'); - - $has_target_bundles = isset($handler_settings['target_bundles']) && !empty($handler_settings['target_bundles']); - $target_bundles = $has_target_bundles ? - $handler_settings['target_bundles'] - : $this->getAllBundlesForEntityType($entity_type_id); - - return array_map(function ($bundle) use ($entity_type_id) { - return $this->resourceTypeRepository->get($entity_type_id, $bundle); - }, $target_bundles); + return $relatable_resource_types; } /** @@ -622,19 +596,6 @@ protected function resourceTypesAreTraversable(array $resource_types) { return FALSE; } - /** - * Gets all bundle IDs for a given entity type. - * - * @param string $entity_type_id - * The entity type for which to get bundles. - * - * @return string[] - * The bundle IDs. - */ - protected function getAllBundlesForEntityType($entity_type_id) { - return array_keys($this->entityTypeBundleInfo->getBundleInfo($entity_type_id)); - } - /** * Gets all unique reference property names from the given field definitions. * diff --git a/core/modules/jsonapi/src/Controller/EntityResource.php b/core/modules/jsonapi/src/Controller/EntityResource.php index b0febdc49b7c..162ce068bf91 100644 --- a/core/modules/jsonapi/src/Controller/EntityResource.php +++ b/core/modules/jsonapi/src/Controller/EntityResource.php @@ -47,6 +47,7 @@ use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel; use Drupal\jsonapi\ResourceResponse; use Drupal\jsonapi\ResourceType\ResourceType; +use Drupal\jsonapi\ResourceType\ResourceTypeField; use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface; use Drupal\jsonapi\Revisions\ResourceVersionRouteEnhancer; use Symfony\Component\HttpFoundation\Request; @@ -236,24 +237,22 @@ public function createIndividual(ResourceType $resource_type, Request $request) // by the user. Field access makes no distinction between 'create' and // 'update', so the 'edit' operation is used here. $document = Json::decode($request->getContent()); + $field_mapping = array_map(function (ResourceTypeField $field) { + return $field->getPublicName(); + }, $resource_type->getFields()); + // User resource objects contain a read-only attribute that is not a + // real field on the user entity type. + // @see \Drupal\jsonapi\JsonApiResource\ResourceObject::extractContentEntityFields() + // @todo: eliminate this special casing in https://www.drupal.org/project/drupal/issues/3079254. + if ($resource_type->getEntityTypeId() === 'user') { + $field_mapping = array_diff($field_mapping, [$resource_type->getPublicName('display_name')]); + } foreach (['attributes', 'relationships'] as $data_member_name) { if (isset($document['data'][$data_member_name])) { - $valid_names = array_filter(array_map(function ($public_field_name) use ($resource_type) { - return $resource_type->getInternalName($public_field_name); - }, array_keys($document['data'][$data_member_name])), function ($internal_field_name) use ($resource_type) { - return $resource_type->hasField($internal_field_name); - }); - // User resource objects contain a read-only attribute that is not a - // real field on the user entity type. - // @see \Drupal\jsonapi\JsonApiResource\ResourceObject::extractContentEntityFields() - // @todo: eliminate this special casing in https://www.drupal.org/project/drupal/issues/3079254. - if ($resource_type->getEntityTypeId() === 'user') { - $valid_names = array_diff($valid_names, [$resource_type->getPublicName('display_name')]); - } - foreach ($valid_names as $field_name) { - $field_access = $parsed_entity->get($field_name)->access('edit', NULL, TRUE); + foreach (array_intersect_key(array_flip($field_mapping), $document['data'][$data_member_name]) as $internal_field_name) { + $field_access = $parsed_entity->get($internal_field_name)->access('edit', NULL, TRUE); if (!$field_access->isAllowed()) { - $public_field_name = $resource_type->getPublicName($field_name); + $public_field_name = $field_mapping[$internal_field_name]; throw new EntityAccessDeniedHttpException(NULL, $field_access, "/data/$data_member_name/$public_field_name", sprintf('The current user is not allowed to POST the selected field (%s).', $public_field_name)); } } @@ -512,7 +511,8 @@ protected function executeQueryInRenderContext(QueryInterface $query, CacheableM */ public function getRelated(ResourceType $resource_type, FieldableEntityInterface $entity, $related, Request $request) { /* @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $field_list */ - $field_list = $entity->get($resource_type->getInternalName($related)); + $resource_relationship = $resource_type->getFieldByPublicName($related); + $field_list = $entity->get($resource_relationship->getInternalName()); // Remove the entities pointing to a resource that may be disabled. Even // though the normalizer skips disabled references, we can avoid unnecessary @@ -531,7 +531,7 @@ function (EntityInterface $entity) { foreach ($referenced_entities as $referenced_entity) { $collection_data[] = $this->entityAccessChecker->getAccessCheckedResourceObject($referenced_entity); } - $primary_data = new ResourceObjectData($collection_data, $field_list->getFieldDefinition()->getFieldStorageDefinition()->getCardinality()); + $primary_data = new ResourceObjectData($collection_data, $resource_relationship->hasOne() ? 1 : -1); $response = $this->buildWrappedResponse($primary_data, $request, $this->getIncludes($request, $primary_data)); // $response does not contain the entity list cache tag. We add the @@ -598,11 +598,11 @@ public function getRelationship(ResourceType $resource_type, FieldableEntityInte */ public function addToRelationshipData(ResourceType $resource_type, FieldableEntityInterface $entity, $related, Request $request) { $resource_identifiers = $this->deserialize($resource_type, $request, ResourceIdentifier::class, $related); - $related = $resource_type->getInternalName($related); + $internal_relationship_field_name = $resource_type->getInternalName($related); // According to the specification, you are only allowed to POST to a // relationship if it is a to-many relationship. /* @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $field_list */ - $field_list = $entity->{$related}; + $field_list = $entity->{$internal_relationship_field_name}; /* @var \Drupal\field\Entity\FieldConfig $field_definition */ $field_definition = $field_list->getFieldDefinition(); $is_multiple = $field_definition->getFieldStorageDefinition()->isMultiple(); @@ -662,12 +662,12 @@ public function addToRelationshipData(ResourceType $resource_type, FieldableEnti */ public function replaceRelationshipData(ResourceType $resource_type, EntityInterface $entity, $related, Request $request) { $resource_identifiers = $this->deserialize($resource_type, $request, ResourceIdentifier::class, $related); - $related = $resource_type->getInternalName($related); + $internal_relationship_field_name = $resource_type->getInternalName($related); /* @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $resource_identifiers */ // According to the specification, PATCH works a little bit different if the // relationship is to-one or to-many. /* @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $field_list */ - $field_list = $entity->{$related}; + $field_list = $entity->{$internal_relationship_field_name}; $field_definition = $field_list->getFieldDefinition(); $is_multiple = $field_definition->getFieldStorageDefinition()->isMultiple(); $method = $is_multiple ? 'doPatchMultipleRelationship' : 'doPatchIndividualRelationship'; @@ -745,8 +745,9 @@ protected function doPatchMultipleRelationship(EntityInterface $entity, array $r */ public function removeFromRelationshipData(ResourceType $resource_type, EntityInterface $entity, $related, Request $request) { $resource_identifiers = $this->deserialize($resource_type, $request, ResourceIdentifier::class, $related); + $internal_relationship_field_name = $resource_type->getInternalName($related); /* @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $field_list */ - $field_list = $entity->{$related}; + $field_list = $entity->{$internal_relationship_field_name}; $is_multiple = $field_list->getFieldDefinition() ->getFieldStorageDefinition() ->isMultiple(); diff --git a/core/modules/jsonapi/src/Controller/FileUpload.php b/core/modules/jsonapi/src/Controller/FileUpload.php index b07dd51d0de4..27032eaf3a8a 100644 --- a/core/modules/jsonapi/src/Controller/FileUpload.php +++ b/core/modules/jsonapi/src/Controller/FileUpload.php @@ -128,7 +128,7 @@ public function handleFileUploadForExistingResource(Request $request, ResourceTy throw new UnprocessableEntityHttpException($message); } - if ($field_definition->getFieldStorageDefinition()->getCardinality() === 1) { + if ($resource_type->getFieldByInternalName($file_field_name)->hasOne()) { $entity->{$file_field_name} = $file; } else { diff --git a/core/modules/jsonapi/src/JsonApiResource/Relationship.php b/core/modules/jsonapi/src/JsonApiResource/Relationship.php index 0f6f9a6325ec..17975f810d73 100644 --- a/core/modules/jsonapi/src/JsonApiResource/Relationship.php +++ b/core/modules/jsonapi/src/JsonApiResource/Relationship.php @@ -117,11 +117,10 @@ protected function __construct($public_field_name, RelationshipData $data, LinkC */ public static function createFromEntityReferenceField(ResourceObject $context, EntityReferenceFieldItemListInterface $field, LinkCollection $links = NULL, array $meta = []) { $context_resource_type = $context->getResourceType(); - $public_field_name = $context_resource_type->getPublicName($field->getName()); - $field_cardinality = $field->getFieldDefinition()->getFieldStorageDefinition()->getCardinality(); + $resource_field = $context_resource_type->getFieldByInternalName($field->getName()); return new static( - $public_field_name, - new RelationshipData(ResourceIdentifier::toResourceIdentifiers($field), $field_cardinality), + $resource_field->getPublicName(), + new RelationshipData(ResourceIdentifier::toResourceIdentifiers($field), $resource_field->hasOne() ? 1 : -1), static::buildLinkCollectionFromEntityReferenceField($context, $field, $links ?: new LinkCollection([])), $meta, $context @@ -228,7 +227,7 @@ public function getMergedMeta(array $top_level_meta) { */ protected static function buildLinkCollectionFromEntityReferenceField(ResourceObject $context, EntityReferenceFieldItemListInterface $field, LinkCollection $links) { $context_resource_type = $context->getResourceType(); - $public_field_name = $context_resource_type->getPublicName($context_resource_type->getPublicName($field->getName())); + $public_field_name = $context_resource_type->getPublicName($field->getName()); if ($context_resource_type->isLocatable() && !$context_resource_type->isInternal()) { $context_is_versionable = $context_resource_type->isVersionable(); if (!$links->hasLinkWithKey('self')) { diff --git a/core/modules/jsonapi/src/Normalizer/EntityReferenceFieldNormalizer.php b/core/modules/jsonapi/src/Normalizer/EntityReferenceFieldNormalizer.php new file mode 100644 index 000000000000..c79d56ae1ae8 --- /dev/null +++ b/core/modules/jsonapi/src/Normalizer/EntityReferenceFieldNormalizer.php @@ -0,0 +1,118 @@ +<?php + +namespace Drupal\jsonapi\Normalizer; + +use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\Field\EntityReferenceFieldItemListInterface; +use Drupal\Core\Url; +use Drupal\jsonapi\JsonApiResource\ResourceIdentifier; +use Drupal\jsonapi\JsonApiResource\ResourceIdentifierInterface; +use Drupal\jsonapi\JsonApiResource\ResourceObject; +use Drupal\jsonapi\JsonApiSpec; +use Drupal\jsonapi\Normalizer\Value\CacheableNormalization; +use Drupal\jsonapi\ResourceType\ResourceTypeRelationship; +use Drupal\jsonapi\Routing\Routes; + +/** + * Normalizer class specific for entity reference field objects. + * + * @internal JSON:API maintains no PHP API since its API is the HTTP API. This + * class may change at any time and this will break any dependencies on it. + * + * @see https://www.drupal.org/project/jsonapi/issues/3032787 + * @see jsonapi.api.php + */ +class EntityReferenceFieldNormalizer extends FieldNormalizer { + + /** + * {@inheritdoc} + */ + protected $supportedInterfaceOrClass = EntityReferenceFieldItemListInterface::class; + + /** + * {@inheritdoc} + */ + public function normalize($field, $format = NULL, array $context = []) { + assert($field instanceof EntityReferenceFieldItemListInterface); + // Build the relationship object based on the Entity Reference and normalize + // that object instead. + $resource_identifiers = array_filter(ResourceIdentifier::toResourceIdentifiers($field->filterEmptyItems()), function (ResourceIdentifierInterface $resource_identifier) { + return !$resource_identifier->getResourceType()->isInternal(); + }); + $normalized_items = CacheableNormalization::aggregate($this->serializer->normalize($resource_identifiers, $format, $context)); + assert($context['resource_object'] instanceof ResourceObject); + $resource_relationship = $context['resource_object']->getResourceType()->getFieldByInternalName($field->getName()); + assert($resource_relationship instanceof ResourceTypeRelationship); + $link_cacheability = new CacheableMetadata(); + $links = array_map(function (Url $link) use ($link_cacheability) { + $href = $link->setAbsolute()->toString(TRUE); + $link_cacheability->addCacheableDependency($href); + return ['href' => $href->getGeneratedUrl()]; + }, static::getRelationshipLinks($context['resource_object'], $resource_relationship)); + $data_normalization = $normalized_items->getNormalization(); + $normalization = [ + // Empty 'to-one' relationships must be NULL. + // Empty 'to-many' relationships must be an empty array. + // @link http://jsonapi.org/format/#document-resource-object-linkage + 'data' => $resource_relationship->hasOne() ? array_shift($data_normalization) : $data_normalization, + ]; + if (!empty($links)) { + $normalization['links'] = $links; + } + return (new CacheableNormalization($normalized_items, $normalization))->withCacheableDependency($link_cacheability); + } + + /** + * Gets the links for the relationship. + * + * @param \Drupal\jsonapi\JsonApiResource\ResourceObject $relationship_context + * The JSON:API resource object context of the relationship. + * @param \Drupal\jsonapi\ResourceType\ResourceTypeRelationship $resource_relationship + * The resource type relationship field. + * + * @return array + * The relationship's links. + */ + public static function getRelationshipLinks(ResourceObject $relationship_context, ResourceTypeRelationship $resource_relationship) { + $resource_type = $relationship_context->getResourceType(); + if ($resource_type->isInternal() || !$resource_type->isLocatable()) { + return []; + } + $public_field_name = $resource_relationship->getPublicName(); + $relationship_route_name = Routes::getRouteName($resource_type, "$public_field_name.relationship.get"); + $links = [ + 'self' => Url::fromRoute($relationship_route_name, ['entity' => $relationship_context->getId()]), + ]; + if (static::hasNonInternalResourceType($resource_type->getRelatableResourceTypesByField($public_field_name))) { + $related_route_name = Routes::getRouteName($resource_type, "$public_field_name.related"); + $links['related'] = Url::fromRoute($related_route_name, ['entity' => $relationship_context->getId()]); + } + if ($resource_type->isVersionable()) { + $version_query_parameter = [JsonApiSpec::VERSION_QUERY_PARAMETER => $relationship_context->getVersionIdentifier()]; + $links['self']->setOption('query', $version_query_parameter); + if (isset($links['related'])) { + $links['related']->setOption('query', $version_query_parameter); + } + } + return $links; + } + + /** + * Determines if a given list of resource types contains a non-internal type. + * + * @param \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types + * The JSON:API resource types to evaluate. + * + * @return bool + * FALSE if every resource type is internal, TRUE otherwise. + */ + protected static function hasNonInternalResourceType(array $resource_types) { + foreach ($resource_types as $resource_type) { + if (!$resource_type->isInternal()) { + return TRUE; + } + } + return FALSE; + } + +} diff --git a/core/modules/jsonapi/src/Normalizer/FieldNormalizer.php b/core/modules/jsonapi/src/Normalizer/FieldNormalizer.php index 3bd46a601bb9..b33677c17867 100644 --- a/core/modules/jsonapi/src/Normalizer/FieldNormalizer.php +++ b/core/modules/jsonapi/src/Normalizer/FieldNormalizer.php @@ -4,7 +4,9 @@ use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; +use Drupal\jsonapi\JsonApiResource\ResourceObject; use Drupal\jsonapi\Normalizer\Value\CacheableNormalization; +use Drupal\jsonapi\ResourceType\ResourceType; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; /** @@ -31,12 +33,8 @@ class FieldNormalizer extends NormalizerBase implements DenormalizerInterface { public function normalize($field, $format = NULL, array $context = []) { /* @var \Drupal\Core\Field\FieldItemListInterface $field */ $normalized_items = $this->normalizeFieldItems($field, $format, $context); - - $cardinality = $field->getFieldDefinition() - ->getFieldStorageDefinition() - ->getCardinality(); - - return $cardinality === 1 + assert($context['resource_object'] instanceof ResourceObject); + return $context['resource_object']->getResourceType()->getFieldByInternalName($field->getName())->hasOne() ? array_shift($normalized_items) ?: CacheableNormalization::permanent(NULL) : CacheableNormalization::aggregate($normalized_items); } @@ -47,6 +45,8 @@ public function normalize($field, $format = NULL, array $context = []) { public function denormalize($data, $class, $format = NULL, array $context = []) { $field_definition = $context['field_definition']; assert($field_definition instanceof FieldDefinitionInterface); + $resource_type = $context['resource_type']; + assert($resource_type instanceof ResourceType); // If $data contains items (recognizable by numerical array keys, which // Drupal's Field API calls "deltas"), then it already is itemized; it's not @@ -61,7 +61,7 @@ public function denormalize($data, $class, $format = NULL, array $context = []) // Single-cardinality fields don't need itemization. $field_item_class = $field_definition->getItemDefinition()->getClass(); - if (count($itemized_data) === 1 && $field_definition->getFieldStorageDefinition()->getCardinality() === 1) { + if (count($itemized_data) === 1 && $resource_type->getFieldByInternalName($field_definition->getName())->hasOne()) { return $this->serializer->denormalize($itemized_data[0], $field_item_class, $format, $context); } diff --git a/core/modules/jsonapi/src/ResourceType/ResourceType.php b/core/modules/jsonapi/src/ResourceType/ResourceType.php index 24a883547ccc..15aea30aafb1 100644 --- a/core/modules/jsonapi/src/ResourceType/ResourceType.php +++ b/core/modules/jsonapi/src/ResourceType/ResourceType.php @@ -2,6 +2,10 @@ namespace Drupal\jsonapi\ResourceType; +use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\TypedData\DataReferenceTargetDefinition; + /** * Value object containing all metadata for a JSON:API resource type. * @@ -82,28 +86,19 @@ class ResourceType { protected $fields; /** - * The list of disabled fields. Disabled by default: uuid, id, type. - * - * @var string[] + * An array of arrays of relatable resource types, keyed by public field name. * - * @see \Drupal\jsonapi\ResourceType\ResourceTypeRepository::getFieldMapping() + * @var array */ - protected $disabledFields; + protected $relatableResourceTypesByField; /** - * The mapping for field aliases: keys=internal names, values=public names. + * The mapping for field aliases: keys=public names, values=internal names. * * @var string[] */ protected $fieldMapping; - /** - * The inverse of $fieldMapping. - * - * @var string[] - */ - protected $invertedFieldMapping; - /** * Gets the entity type ID. * @@ -160,8 +155,8 @@ public function getDeserializationTargetClass() { */ public function getPublicName($field_name) { // By default the entity field name is the public field name. - return isset($this->fieldMapping[$field_name]) - ? $this->fieldMapping[$field_name] + return isset($this->fields[$field_name]) + ? $this->fields[$field_name]->getPublicName() : $field_name; } @@ -176,9 +171,47 @@ public function getPublicName($field_name) { */ public function getInternalName($field_name) { // By default the entity field name is the public field name. - return isset($this->invertedFieldMapping[$field_name]) - ? $this->invertedFieldMapping[$field_name] - : $field_name; + return $this->fieldMapping[$field_name] ?? $field_name; + } + + /** + * Gets the attribute and relationship fields of this resource type. + * + * @return \Drupal\jsonapi\ResourceType\ResourceTypeField[] + * The field objects on this resource type. + */ + public function getFields() { + return $this->fields; + } + + /** + * Gets a particular attribute or relationship field by public field name. + * + * @param string $public_field_name + * The public field name of the desired field. + * + * @return \Drupal\jsonapi\ResourceType\ResourceTypeField|null + * A resource type field object or NULL if the field does not exist on this + * resource type. + */ + public function getFieldByPublicName($public_field_name) { + return isset($this->fieldMapping[$public_field_name]) + ? $this->getFieldByInternalName($this->fieldMapping[$public_field_name]) + : NULL; + } + + /** + * Gets a particular attribute or relationship field by internal field name. + * + * @param string $internal_field_name + * The internal field name of the desired field. + * + * @return \Drupal\jsonapi\ResourceType\ResourceTypeField|null + * A resource type field object or NULL if the field does not exist on this + * resource type. + */ + public function getFieldByInternalName($internal_field_name) { + return $this->fields[$internal_field_name] ?? NULL; } /** @@ -199,7 +232,7 @@ public function getInternalName($field_name) { * otherwise. */ public function hasField($field_name) { - return in_array($field_name, $this->fields, TRUE); + return array_key_exists($field_name, $this->fields); } /** @@ -216,7 +249,7 @@ public function hasField($field_name) { * of the data model. FALSE otherwise. */ public function isFieldEnabled($field_name) { - return $this->hasField($field_name) && !in_array($field_name, $this->disabledFields, TRUE); + return $this->hasField($field_name) && $this->fields[$field_name]->isFieldEnabled(); } /** @@ -307,10 +340,13 @@ public function isVersionable() { * (optional) Whether the resource type is mutable. * @param bool $is_versionable * (optional) Whether the resource type is versionable. - * @param array $field_mapping - * (optional) The field mapping to use. + * @param \Drupal\jsonapi\ResourceType\ResourceTypeField[] $fields + * (optional) The resource type fields, keyed by internal field name. */ - public function __construct($entity_type_id, $bundle, $deserialization_target_class, $internal = FALSE, $is_locatable = TRUE, $is_mutable = TRUE, $is_versionable = FALSE, array $field_mapping = []) { + public function __construct($entity_type_id, $bundle, $deserialization_target_class, $internal = FALSE, $is_locatable = TRUE, $is_mutable = TRUE, $is_versionable = FALSE, array $fields = []) { + if (!empty($fields) && !reset($fields) instanceof ResourceTypeField) { + $fields = $this->updateDeprecatedFieldMapping($fields, $entity_type_id, $bundle); + } $this->entityTypeId = $entity_type_id; $this->bundle = $bundle; $this->deserializationTargetClass = $deserialization_target_class; @@ -318,17 +354,15 @@ public function __construct($entity_type_id, $bundle, $deserialization_target_cl $this->isLocatable = $is_locatable; $this->isMutable = $is_mutable; $this->isVersionable = $is_versionable; + $this->fields = $fields; $this->typeName = $this->bundle === '?' ? 'unknown' : sprintf('%s--%s', $this->entityTypeId, $this->bundle); - $this->fields = array_keys($field_mapping); - $this->disabledFields = array_keys(array_filter($field_mapping, function ($v) { - return $v === FALSE; - })); - $this->fieldMapping = array_filter($field_mapping, 'is_string'); - $this->invertedFieldMapping = array_flip($this->fieldMapping); + $this->fieldMapping = array_flip(array_map(function (ResourceTypeField $field) { + return $field->getPublicName(); + }, $this->fields)); } /** @@ -341,7 +375,16 @@ public function __construct($entity_type_id, $bundle, $deserialization_target_cl * across resource types across fields, but not within a field. */ public function setRelatableResourceTypes(array $relatable_resource_types) { - $this->relatableResourceTypes = $relatable_resource_types; + $this->fields = array_reduce(array_keys($relatable_resource_types), function ($fields, $public_field_name) use ($relatable_resource_types) { + if (!isset($this->fieldMapping[$public_field_name])) { + throw new \LogicException('A field must exist for relatable resource types to be set on it.'); + } + $internal_field_name = $this->fieldMapping[$public_field_name]; + $field = $fields[$internal_field_name]; + assert($field instanceof ResourceTypeRelationship); + $fields[$internal_field_name] = $field->withRelatableResourceTypes($relatable_resource_types[$public_field_name]); + return $fields; + }, $this->fields); } /** @@ -353,10 +396,14 @@ public function setRelatableResourceTypes(array $relatable_resource_types) { * @see self::setRelatableResourceTypes() */ public function getRelatableResourceTypes() { - if (!isset($this->relatableResourceTypes)) { - throw new \LogicException("setRelatableResourceTypes() must be called before getting relatable resource types."); + if (!isset($this->relatableResourceTypesByField)) { + $this->relatableResourceTypesByField = array_reduce(array_map(function (ResourceTypeRelationship $field) { + return [$field->getPublicName() => $field->getRelatableResourceTypes()]; + }, array_filter($this->fields, function (ResourceTypeField $field) { + return $field instanceof ResourceTypeRelationship; + })), 'array_merge', []); } - return $this->relatableResourceTypes; + return $this->relatableResourceTypesByField; } /** @@ -371,10 +418,9 @@ public function getRelatableResourceTypes() { * @see self::getRelatableResourceTypes() */ public function getRelatableResourceTypesByField($field_name) { - $relatable_resource_types = $this->getRelatableResourceTypes(); - return isset($relatable_resource_types[$field_name]) ? - $relatable_resource_types[$field_name] : - []; + return ($field = $this->getFieldByPublicName($field_name)) && $field instanceof ResourceTypeRelationship + ? $field->getRelatableResourceTypes() + : []; } /** @@ -389,4 +435,92 @@ public function getPath() { return sprintf('/%s/%s', $this->getEntityTypeId(), $this->getBundle()); } + /** + * {@inheritdoc} + */ + public function __get($name) { + $class_name = self::class; + @trigger_error("Using the ${$name} protected property of a {$class_name} is deprecated in Drupal 8.8.0 and will not be allowed in Drupal 9.0.0. Use {$class_name}::getFields() instead. See https://www.drupal.org/node/3084721.", E_USER_DEPRECATED); + if ($name === 'disabledFields') { + return array_map(function (ResourceTypeField $field) { + return $field->getInternalName(); + }, array_filter($this->getFields(), function (ResourceTypeField $field) { + return !$field->isFieldEnabled(); + })); + } + if ($name === 'invertedFieldMapping') { + return array_reduce($this->getFields(), function ($inverted_field_mapping, ResourceTypeField $field) { + $internal_field_name = $field->getInternalName(); + $public_field_name = $field->getPublicName(); + if ($field->isFieldEnabled() && $internal_field_name !== $public_field_name) { + $inverted_field_mapping[$public_field_name] = $internal_field_name; + } + return $inverted_field_mapping; + }, []); + } + } + + /** + * Takes a deprecated field mapping and converts it to ResourceTypeFields. + * + * @param array $field_mapping + * The deprecated field mapping. + * @param string $entity_type_id + * The entity type ID of the field mapping. + * @param string $bundle + * The bundle ID of the field mapping or the entity type ID if the entity + * type does not have bundles. + * + * @return \Drupal\jsonapi\ResourceType\ResourceTypeField[] + * The updated field mapping objects. + * + * @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use + * self::getFields() instead. + * + * @see https://www.drupal.org/project/drupal/issues/3014277 + */ + private function updateDeprecatedFieldMapping(array $field_mapping, $entity_type_id, $bundle) { + $class_name = self::class; + @trigger_error("Passing an array with strings or booleans as a field mapping to {$class_name}::__construct() is deprecated in Drupal 8.8.0 and will not be allowed in Drupal 9.0.0. See \Drupal\jsonapi\ResourceTypeRepository::getFields(). See https://www.drupal.org/node/3084746.", E_USER_DEPRECATED); + + // See \Drupal\jsonapi\ResourceType\ResourceTypeRepository::isReferenceFieldDefinition(). + $is_reference_field_definition = function (FieldDefinitionInterface $field_definition) { + static $field_type_is_reference = []; + + if (isset($field_type_is_reference[$field_definition->getType()])) { + return $field_type_is_reference[$field_definition->getType()]; + } + + /* @var \Drupal\Core\Field\TypedData\FieldItemDataDefinition $item_definition */ + $item_definition = $field_definition->getItemDefinition(); + $main_property = $item_definition->getMainPropertyName(); + $property_definition = $item_definition->getPropertyDefinition($main_property); + + return $field_type_is_reference[$field_definition->getType()] = $property_definition instanceof DataReferenceTargetDefinition; + }; + + /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager */ + $entity_type_manager = \Drupal::service('entity_type.manager'); + $is_fieldable = $entity_type_manager->getDefinition($entity_type_manager)->entityClassImplements(FieldableEntityInterface::class); + $field_definitions = $is_fieldable + ? \Drupal::service('entity_field.manager')->getFieldDefinitions($entity_type_id, $bundle) + : []; + + $fields = []; + foreach ($field_mapping as $internal_field_name => $public_field_name) { + assert(is_bool($public_field_name) || is_string($public_field_name)); + $field_definition = $is_fieldable && !empty($field_definitions[$internal_field_name]) + ? $field_definitions[$internal_field_name] + : NULL; + $is_relationship_field = $field_definition && $is_reference_field_definition($field_definition); + $has_one = !$field_definition || $field_definition->getFieldStorageDefinition()->getCardinality() === 1; + $alias = is_string($public_field_name) ? $public_field_name : NULL; + $fields[$internal_field_name] = $is_relationship_field + ? new ResourceTypeRelationship($internal_field_name, $alias, $public_field_name !== FALSE, $has_one) + : new ResourceTypeAttribute($internal_field_name, $alias, $public_field_name !== FALSE, $has_one); + } + + return $fields; + } + } diff --git a/core/modules/jsonapi/src/ResourceType/ResourceTypeAttribute.php b/core/modules/jsonapi/src/ResourceType/ResourceTypeAttribute.php new file mode 100644 index 000000000000..19f71c192f69 --- /dev/null +++ b/core/modules/jsonapi/src/ResourceType/ResourceTypeAttribute.php @@ -0,0 +1,16 @@ +<?php + +namespace Drupal\jsonapi\ResourceType; + +/** + * Specialization of a ResourceTypeField to represent a resource type attribute. + * + * @internal JSON:API maintains no PHP API since its API is the HTTP API. This + * class may change at any time and this will break any dependencies on it. + * + * @see https://www.drupal.org/project/jsonapi/issues/3032787 + * @see jsonapi.api.php + * + * @see \Drupal\jsonapi\ResourceType\ResourceTypeRepository + */ +class ResourceTypeAttribute extends ResourceTypeField {} diff --git a/core/modules/jsonapi/src/ResourceType/ResourceTypeField.php b/core/modules/jsonapi/src/ResourceType/ResourceTypeField.php new file mode 100644 index 000000000000..76e35dbcc0c8 --- /dev/null +++ b/core/modules/jsonapi/src/ResourceType/ResourceTypeField.php @@ -0,0 +1,129 @@ +<?php + +namespace Drupal\jsonapi\ResourceType; + +/** + * Abstract value object containing all metadata for a JSON:API resource field. + * + * @internal JSON:API maintains no PHP API since its API is the HTTP API. This + * class may change at any time and this will break any dependencies on it. + * + * @see https://www.drupal.org/project/jsonapi/issues/3032787 + * @see jsonapi.api.php + * + * @see \Drupal\jsonapi\ResourceType\ResourceTypeRepository + */ +abstract class ResourceTypeField { + + /** + * The internal field name. + * + * @var string + */ + protected $internalName; + + /** + * The public field name. + * + * @var string + */ + protected $publicName; + + /** + * Whether the field is disabled. + * + * @var bool + */ + protected $enabled; + + /** + * Whether the field can only have one value. + * + * @var bool + */ + protected $hasOne; + + /** + * ResourceTypeField constructor. + * + * @param string $internal_name + * The internal field name. + * @param string $public_name + * (optional) The public field name. Defaults to the internal field name. + * @param bool $enabled + * (optional) Whether the field is enabled. Defaults to TRUE. + * @param bool $has_one + * (optional) Whether the field can only have ony value. Defaults to TRUE. + */ + public function __construct($internal_name, $public_name = NULL, $enabled = TRUE, $has_one = TRUE) { + $this->internalName = $internal_name; + $this->publicName = $public_name ?: $internal_name; + $this->enabled = $enabled; + $this->hasOne = $has_one; + } + + /** + * Gets the internal name of the field. + * + * @return string + * The internal name of the field. + */ + public function getInternalName() { + return $this->internalName; + } + + /** + * Gets the public name of the field. + * + * @return string + * The public name of the field. + */ + public function getPublicName() { + return $this->publicName; + } + + /** + * Establishes a new public name for the field. + * + * @param string $public_name + * The public name. + * + * @return static + * A new instance of the field with the given public name. + */ + public function withPublicName($public_name) { + return new static($this->internalName, $public_name, $this->enabled, $this->hasOne); + } + + /** + * Whether the field is enabled. + * + * @return bool + * Whether the field is enabled. FALSE if the field should not be in the + * JSON:API response. + */ + public function isFieldEnabled() { + return $this->enabled; + } + + /** + * Whether the field can only have one value. + * + * @return bool + * TRUE if the field can have only one value, FALSE otherwise. + */ + public function hasOne() { + return $this->hasOne; + } + + /** + * Whether the field can have many values. + * + * @return bool + * TRUE if the field can have more than one value, FALSE otherwise. + */ + public function hasMany() { + return !$this->hasOne; + } + +} diff --git a/core/modules/jsonapi/src/ResourceType/ResourceTypeRelationship.php b/core/modules/jsonapi/src/ResourceType/ResourceTypeRelationship.php new file mode 100644 index 000000000000..e62203ca2fe7 --- /dev/null +++ b/core/modules/jsonapi/src/ResourceType/ResourceTypeRelationship.php @@ -0,0 +1,63 @@ +<?php + +namespace Drupal\jsonapi\ResourceType; + +/** + * Specialization of a ResourceTypeField to represent a resource relationship. + * + * @internal JSON:API maintains no PHP API since its API is the HTTP API. This + * class may change at any time and this will break any dependencies on it. + * + * @see https://www.drupal.org/project/jsonapi/issues/3032787 + * @see jsonapi.api.php + * + * @see \Drupal\jsonapi\ResourceType\ResourceTypeRepository + */ +class ResourceTypeRelationship extends ResourceTypeField { + + /** + * The resource type to which this relationships can relate. + * + * @var \Drupal\jsonapi\ResourceType\ResourceType[] + */ + protected $relatableResourceTypes; + + /** + * Establishes the relatable resource types of this field. + * + * @param array $resource_types + * The array of relatable resource types. + * + * @return static + * A new instance of the field with the given relatable resource types. + */ + public function withRelatableResourceTypes(array $resource_types) { + $relationship = new static($this->internalName, $this->publicName, $this->enabled, $this->hasOne); + $relationship->relatableResourceTypes = $resource_types; + return $relationship; + } + + /** + * Gets the relatable resource types. + * + * @return \Drupal\jsonapi\ResourceType\ResourceType[] + * The resource type to which this relationships can relate. + */ + public function getRelatableResourceTypes() { + if (!isset($this->relatableResourceTypes)) { + throw new \LogicException("withRelatableResourceTypes() must be called before getting relatable resource types."); + } + return $this->relatableResourceTypes; + } + + /** + * {@inheritdoc} + */ + public function withPublicName($public_name) { + $relationship = parent::withPublicName($public_name); + return isset($this->relatableResourceTypes) + ? $relationship->withRelatableResourceTypes($this->relatableResourceTypes) + : $relationship; + } + +} diff --git a/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php b/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php index 400d31a6105c..b70ecee04d6a 100644 --- a/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php +++ b/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php @@ -147,7 +147,7 @@ protected function createResourceType(EntityTypeInterface $entity_type, $bundle) static::isLocatableResourceType($entity_type, $bundle), static::isMutableResourceType($entity_type, $bundle), static::isVersionableResourceType($entity_type), - static::getFieldMapping($raw_fields, $entity_type, $bundle) + static::getFields($raw_fields, $entity_type, $bundle) ); } @@ -181,21 +181,14 @@ public function getByTypeName($type_name) { * @param string $bundle * The bundle to assess. * - * @return array - * An array with: - * - keys are (real/internal) field names - * - values are either FALSE (indicating the field is not exposed despite - * not being internal), TRUE (indicating the field should be exposed under - * its internal name) or a string (indicating the field should not be - * exposed using its internal name, but the name specified in the string) + * @return \Drupal\jsonapi\ResourceType\ResourceTypeField[] + * An array of JSON:API resource type fields keyed by internal field names. */ - protected static function getFieldMapping(array $field_names, EntityTypeInterface $entity_type, $bundle) { + protected function getFields(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 = []; - // JSON:API resource identifier objects are sufficient to identify // entities. By exposing all fields as attributes, we expose unwanted, // confusing or duplicate information: @@ -210,12 +203,12 @@ protected static function getFieldMapping(array $field_names, EntityTypeInterfac // @see http://jsonapi.org/format/#document-resource-identifier-objects $id_field_name = $entity_type->getKey('id'); $uuid_field_name = $entity_type->getKey('uuid'); - if ($uuid_field_name !== 'id') { - $mapping[$uuid_field_name] = FALSE; + if ($uuid_field_name && $uuid_field_name !== 'id') { + $fields[$uuid_field_name] = new ResourceTypeAttribute($uuid_field_name, NULL, FALSE); } - $mapping[$id_field_name] = "drupal_internal__$id_field_name"; + $fields[$id_field_name] = new ResourceTypeAttribute($id_field_name, "drupal_internal__$id_field_name"); if ($entity_type->isRevisionable() && ($revision_id_field_name = $entity_type->getKey('revision'))) { - $mapping[$revision_id_field_name] = "drupal_internal__$revision_id_field_name"; + $fields[$revision_id_field_name] = new ResourceTypeAttribute($revision_id_field_name, "drupal_internal__$revision_id_field_name"); } if ($entity_type instanceof ConfigEntityTypeInterface) { // The '_core' key is reserved by Drupal core to handle complex edge cases @@ -223,25 +216,46 @@ protected static function getFieldMapping(array $field_names, EntityTypeInterfac // configuration, and is not allowed to be set by clients writing // configuration: it is for Drupal core only, and managed by Drupal core. // @see https://www.drupal.org/node/2653358 - $mapping['_core'] = FALSE; + $fields['_core'] = new ResourceTypeAttribute('_core', NULL, FALSE); + } + + $is_fieldable = $entity_type->entityClassImplements(FieldableEntityInterface::class); + if ($is_fieldable) { + $field_definitions = $this->entityFieldManager->getFieldDefinitions($entity_type->id(), $bundle); } // For all other fields, use their internal field name also as their public // field name. Unless they're called "id" or "type": those names are // reserved by the JSON:API spec. // @see http://jsonapi.org/format/#document-resource-object-fields - foreach (array_diff($field_names, array_keys($mapping)) as $field_name) { - if ($field_name === 'id' || $field_name === 'type') { + $reserved_field_names = ['id', 'type']; + foreach (array_diff($field_names, array_keys($fields)) as $field_name) { + $alias = $field_name; + // Alias the fields reserved by the JSON:API spec with `{entity_type}_`. + if (in_array($field_name, $reserved_field_names, TRUE)) { $alias = $entity_type->id() . '_' . $field_name; - if (in_array($alias, $field_names, TRUE)) { - throw new \LogicException("The generated alias '{$alias}' for field name '{$field_name}' conflicts with an existing field. Please report this in the JSON:API issue queue!"); - } - $mapping[$field_name] = $alias; - continue; } // The default, which applies to most fields: expose as-is. - $mapping[$field_name] = TRUE; + $field_definition = $is_fieldable && !empty($field_definitions[$field_name]) ? $field_definitions[$field_name] : NULL; + $is_relationship_field = $field_definition && static::isReferenceFieldDefinition($field_definition); + $has_one = !$field_definition || $field_definition->getFieldStorageDefinition()->getCardinality() === 1; + $fields[$field_name] = $is_relationship_field + ? new ResourceTypeRelationship($field_name, $alias, TRUE, $has_one) + : new ResourceTypeAttribute($field_name, $alias, TRUE, $has_one); + } + + // With all fields now aliased, detect any conflicts caused by the + // automatically generated aliases above. + foreach (array_intersect($reserved_field_names, array_keys($fields)) as $reserved_field_name) { + /* @var \Drupal\jsonapi\ResourceType\ResourceTypeField $aliased_reserved_field */ + $aliased_reserved_field = $fields[$reserved_field_name]; + /* @var \Drupal\jsonapi\ResourceType\ResourceTypeField $field */ + foreach (array_diff_key($fields, array_flip([$reserved_field_name])) as $field) { + if ($aliased_reserved_field->getPublicName() === $field->getPublicName()) { + throw new \LogicException("The generated alias '{$aliased_reserved_field->getPublicName()}' for field name '{$aliased_reserved_field->getInternalName()}' conflicts with an existing field. Please report this in the JSON:API issue queue!"); + } + } } // Special handling for user entities that allows a JSON:API user agent to @@ -250,10 +264,47 @@ protected static function getFieldMapping(array $field_names, EntityTypeInterfac // @see \Drupal\jsonapi\JsonApiResource\ResourceObject::extractContentEntityFields() // @todo: eliminate this special casing in https://www.drupal.org/project/drupal/issues/3079254. if ($entity_type->id() === 'user') { - $mapping['display_name'] = TRUE; + $fields['display_name'] = new ResourceTypeAttribute('display_name'); } - return $mapping; + return $fields; + } + + /** + * Gets the field mapping for the given field names and entity type + bundle. + * + * @param string[] $field_names + * All field names on a bundle of the given entity type. + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type for which to get the field mapping. + * @param string $bundle + * The bundle to assess. + * + * @return array + * An array with: + * - keys are (real/internal) field names + * - values are either FALSE (indicating the field is not exposed despite + * not being internal), TRUE (indicating the field should be exposed under + * its internal name) or a string (indicating the field should not be + * exposed using its internal name, but the name specified in the string) + * + * @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use + * self::getFields() instead. + * + * @see https://www.drupal.org/project/drupal/issues/3014277 + */ + protected function getFieldMapping(array $field_names, EntityTypeInterface $entity_type, $bundle) { + $class_name = self::class; + @trigger_error("{$class_name}::getFieldMapping() is deprecated in Drupal 8.8.0 and will not be allowed in Drupal 9.0.0. Use {$class_name}::getFields() instead. See https://www.drupal.org/project/drupal/issues/3014277.", E_USER_DEPRECATED); + $fields = $this->getFields($field_names, $entity_type, $bundle); + return array_map(function (ResourceTypeField $field) { + if ($field->isFieldEnabled()) { + return $field->getInternalName() !== $field->getPublicName() + ? $field->getPublicName() + : TRUE; + } + return FALSE; + }, $fields); } /** diff --git a/core/modules/jsonapi/tests/modules/jsonapi_test_collection_count/src/ResourceType/CountableResourceTypeRepository.php b/core/modules/jsonapi/tests/modules/jsonapi_test_collection_count/src/ResourceType/CountableResourceTypeRepository.php index 33c9c1a4d1c0..39a3974c2501 100644 --- a/core/modules/jsonapi/tests/modules/jsonapi_test_collection_count/src/ResourceType/CountableResourceTypeRepository.php +++ b/core/modules/jsonapi/tests/modules/jsonapi_test_collection_count/src/ResourceType/CountableResourceTypeRepository.php @@ -22,7 +22,8 @@ protected function createResourceType(EntityTypeInterface $entity_type, $bundle) $entity_type->isInternal(), static::isLocatableResourceType($entity_type, $bundle), static::isMutableResourceType($entity_type, $bundle), - static::getFieldMapping($raw_fields, $entity_type, $bundle) + static::isVersionableResourceType($entity_type), + static::getFields($raw_fields, $entity_type, $bundle) ); } diff --git a/core/modules/jsonapi/tests/modules/jsonapi_test_field_aliasing/src/ResourceType/AliasingResourceTypeRepository.php b/core/modules/jsonapi/tests/modules/jsonapi_test_field_aliasing/src/ResourceType/AliasingResourceTypeRepository.php index 59850dad6c82..218deb042320 100644 --- a/core/modules/jsonapi/tests/modules/jsonapi_test_field_aliasing/src/ResourceType/AliasingResourceTypeRepository.php +++ b/core/modules/jsonapi/tests/modules/jsonapi_test_field_aliasing/src/ResourceType/AliasingResourceTypeRepository.php @@ -13,14 +13,14 @@ class AliasingResourceTypeRepository extends ResourceTypeRepository { /** * {@inheritdoc} */ - protected static function getFieldMapping(array $field_names, EntityTypeInterface $entity_type, $bundle) { - $mapping = parent::getFieldMapping($field_names, $entity_type, $bundle); - foreach ($field_names as $field_name) { + protected function getFields(array $field_names, EntityTypeInterface $entity_type, $bundle) { + $fields = parent::getFields($field_names, $entity_type, $bundle); + foreach ($fields as $field_name => $field) { if (strpos($field_name, 'field_test_alias_') === 0) { - $mapping[$field_name] = 'field_test_alias'; + $fields[$field_name] = $fields[$field_name]->withPublicName('field_test_alias'); } } - return $mapping; + return $fields; } } diff --git a/core/modules/jsonapi/tests/src/Kernel/ResourceType/ResourceTypeRepositoryTest.php b/core/modules/jsonapi/tests/src/Kernel/ResourceType/ResourceTypeRepositoryTest.php index 011ef97c85c1..b8c09f0f35eb 100644 --- a/core/modules/jsonapi/tests/src/Kernel/ResourceType/ResourceTypeRepositoryTest.php +++ b/core/modules/jsonapi/tests/src/Kernel/ResourceType/ResourceTypeRepositoryTest.php @@ -122,7 +122,7 @@ public function testMappingNameConflictCheck($field_name_list) { $entity_type = \Drupal::entityTypeManager()->getDefinition('node'); $bundle = 'article'; $reflection_class = new \ReflectionClass($this->resourceTypeRepository); - $reflection_method = $reflection_class->getMethod('getFieldMapping'); + $reflection_method = $reflection_class->getMethod('getFields'); $reflection_method->setAccessible(TRUE); $this->expectException(\LogicException::class); diff --git a/core/modules/jsonapi/tests/src/Kernel/Serializer/SerializerTest.php b/core/modules/jsonapi/tests/src/Kernel/Serializer/SerializerTest.php index 97f0c06be45b..e0cd524b9770 100644 --- a/core/modules/jsonapi/tests/src/Kernel/Serializer/SerializerTest.php +++ b/core/modules/jsonapi/tests/src/Kernel/Serializer/SerializerTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\jsonapi\Kernel\Serializer; use Drupal\Core\Render\Markup; +use Drupal\jsonapi\JsonApiResource\ResourceObject; use Drupal\jsonapi\Normalizer\Value\CacheableNormalization; use Drupal\jsonapi_test_data_type\TraversableObject; use Drupal\node\Entity\Node; @@ -41,6 +42,13 @@ class SerializerTest extends JsonapiKernelTestBase { */ protected $node; + /** + * A resource type for testing. + * + * @var \Drupal\jsonapi\ResourceType\ResourceType + */ + protected $resourceType; + /** * The subject under test. * @@ -80,6 +88,7 @@ protected function setUp() { ]); $this->node->save(); $this->container->setAlias('sut', 'jsonapi.serializer'); + $this->resourceType = $this->container->get('jsonapi.resource_type.repository')->get($this->node->getEntityTypeId(), $this->node->bundle()); $this->sut = $this->container->get('sut'); } @@ -87,7 +96,10 @@ protected function setUp() { * @covers \Drupal\jsonapi\Serializer\Serializer::normalize */ public function testFallbackNormalizer() { - $context = ['account' => $this->user]; + $context = [ + 'account' => $this->user, + 'resource_object' => ResourceObject::createFromEntity($this->resourceType, $this->node), + ]; $value = $this->sut->normalize($this->node->field_text, 'api_json', $context); $this->assertTrue($value instanceof CacheableNormalization); diff --git a/core/modules/jsonapi/tests/src/Unit/Normalizer/ResourceIdentifierNormalizerTest.php b/core/modules/jsonapi/tests/src/Unit/Normalizer/ResourceIdentifierNormalizerTest.php index 390d59d5cda2..8df76d65591a 100644 --- a/core/modules/jsonapi/tests/src/Unit/Normalizer/ResourceIdentifierNormalizerTest.php +++ b/core/modules/jsonapi/tests/src/Unit/Normalizer/ResourceIdentifierNormalizerTest.php @@ -13,6 +13,7 @@ use Drupal\jsonapi\JsonApiResource\ResourceIdentifier; use Drupal\jsonapi\Normalizer\ResourceIdentifierNormalizer; use Drupal\jsonapi\ResourceType\ResourceType; +use Drupal\jsonapi\ResourceType\ResourceTypeRelationship; use Drupal\jsonapi\ResourceType\ResourceTypeRepository; use Drupal\Tests\UnitTestCase; use Prophecy\Argument; @@ -45,7 +46,11 @@ class ResourceIdentifierNormalizerTest extends UnitTestCase { */ public function setUp() { $target_resource_type = new ResourceType('lorem', 'dummy_bundle', NULL); - $this->resourceType = new ResourceType('fake_entity_type', 'dummy_bundle', NULL); + $relationship_fields = [ + 'field_dummy' => new ResourceTypeRelationship('field_dummy'), + 'field_dummy_single' => new ResourceTypeRelationship('field_dummy_single'), + ]; + $this->resourceType = new ResourceType('fake_entity_type', 'dummy_bundle', NULL, FALSE, TRUE, TRUE, FALSE, $relationship_fields); $this->resourceType->setRelatableResourceTypes([ 'field_dummy' => [$target_resource_type], 'field_dummy_single' => [$target_resource_type], diff --git a/core/modules/jsonapi/tests/src/Unit/Routing/RoutesTest.php b/core/modules/jsonapi/tests/src/Unit/Routing/RoutesTest.php index e444a0bd3771..5ec52a6a9277 100644 --- a/core/modules/jsonapi/tests/src/Unit/Routing/RoutesTest.php +++ b/core/modules/jsonapi/tests/src/Unit/Routing/RoutesTest.php @@ -4,6 +4,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\jsonapi\ResourceType\ResourceType; +use Drupal\jsonapi\ResourceType\ResourceTypeRelationship; use Drupal\jsonapi\ResourceType\ResourceTypeRepository; use Drupal\jsonapi\Routing\Routes; use Drupal\Tests\UnitTestCase; @@ -30,8 +31,13 @@ class RoutesTest extends UnitTestCase { */ protected function setUp() { parent::setUp(); - $type_1 = new ResourceType('entity_type_1', 'bundle_1_1', EntityInterface::class); - $type_2 = new ResourceType('entity_type_2', 'bundle_2_1', EntityInterface::class, TRUE); + $relationship_fields = [ + 'external' => new ResourceTypeRelationship('external'), + 'internal' => new ResourceTypeRelationship('internal'), + 'both' => new ResourceTypeRelationship('both'), + ]; + $type_1 = new ResourceType('entity_type_1', 'bundle_1_1', EntityInterface::class, FALSE, TRUE, TRUE, FALSE, $relationship_fields); + $type_2 = new ResourceType('entity_type_2', 'bundle_2_1', EntityInterface::class, TRUE, TRUE, TRUE, FALSE, $relationship_fields); $relatable_resource_types = [ 'external' => [$type_1], 'internal' => [$type_2], -- GitLab