diff --git a/core/modules/jsonapi/jsonapi.services.yml b/core/modules/jsonapi/jsonapi.services.yml index b5cf0ef1e90de5fa3546044143b8c339c550bed8..319b3c1927884efb704e419ef17726a287e5754f 100644 --- a/core/modules/jsonapi/jsonapi.services.yml +++ b/core/modules/jsonapi/jsonapi.services.yml @@ -72,11 +72,10 @@ services: class: Drupal\jsonapi\Normalizer\LinkCollectionNormalizer tags: - { name: jsonapi_normalizer } - serializer.normalizer.entity_reference_field.jsonapi: - class: Drupal\jsonapi\Normalizer\EntityReferenceFieldNormalizer + serializer.normalizer.relationship.jsonapi: + class: Drupal\jsonapi\Normalizer\RelationshipNormalizer tags: - # This must have a higher priority than the 'serializer.normalizer.field.jsonapi' to take effect. - - { name: jsonapi_normalizer, priority: 1 } + - { name: jsonapi_normalizer } serializer.encoder.jsonapi: class: Drupal\jsonapi\Encoder\JsonEncoder tags: diff --git a/core/modules/jsonapi/src/Controller/EntityResource.php b/core/modules/jsonapi/src/Controller/EntityResource.php index 24c0dadf7a4cda63b29153346b618a467be52c4c..b0febdc49b7c5d82bf3c7ad4470e32c3b0ca75da 100644 --- a/core/modules/jsonapi/src/Controller/EntityResource.php +++ b/core/modules/jsonapi/src/Controller/EntityResource.php @@ -34,11 +34,12 @@ use Drupal\jsonapi\JsonApiResource\IncludedData; use Drupal\jsonapi\JsonApiResource\LinkCollection; use Drupal\jsonapi\JsonApiResource\NullIncludedData; +use Drupal\jsonapi\JsonApiResource\Relationship; use Drupal\jsonapi\JsonApiResource\ResourceIdentifier; use Drupal\jsonapi\JsonApiResource\Link; use Drupal\jsonapi\JsonApiResource\ResourceObject; use Drupal\jsonapi\JsonApiResource\ResourceObjectData; -use Drupal\jsonapi\Normalizer\EntityReferenceFieldNormalizer; +use Drupal\jsonapi\JsonApiResource\TopLevelDataInterface; use Drupal\jsonapi\Query\Filter; use Drupal\jsonapi\Query\Sort; use Drupal\jsonapi\Query\OffsetPage; @@ -563,10 +564,8 @@ public function getRelationship(ResourceType $resource_type, FieldableEntityInte // Access will have already been checked by the RelationshipFieldAccess // service, so we don't need to call ::getAccessCheckedResourceObject(). $resource_object = ResourceObject::createFromEntity($resource_type, $entity); - $relationship_object_urls = EntityReferenceFieldNormalizer::getRelationshipLinks($resource_object, $related); - $response = $this->buildWrappedResponse($field_list, $request, $this->getIncludes($request, $resource_object), $response_code, [], array_reduce(array_keys($relationship_object_urls), function (LinkCollection $links, $key) use ($relationship_object_urls) { - return $links->withLink($key, new Link(new CacheableMetadata(), $relationship_object_urls[$key], [$key])); - }, new LinkCollection([]))); + $relationship = Relationship::createFromEntityReferenceField($resource_object, $field_list); + $response = $this->buildWrappedResponse($relationship, $request, $this->getIncludes($request, $resource_object), $response_code); // Add the host entity as a cacheable dependency. $response->addCacheableDependency($entity); return $response; @@ -969,7 +968,7 @@ protected static function relationshipResponseRequiresBody(array $received_resou /** * Builds a response with the appropriate wrapped document. * - * @param mixed $data + * @param \Drupal\jsonapi\JsonApiResource\TopLevelDataInterface $data * The data to wrap. * @param \Symfony\Component\HttpFoundation\Request $request * The request object. @@ -988,8 +987,7 @@ protected static function relationshipResponseRequiresBody(array $received_resou * @return \Drupal\jsonapi\ResourceResponse * The response. */ - protected function buildWrappedResponse($data, Request $request, IncludedData $includes, $response_code = 200, array $headers = [], LinkCollection $links = NULL, array $meta = []) { - assert($data instanceof Data || $data instanceof FieldItemListInterface); + protected function buildWrappedResponse(TopLevelDataInterface $data, Request $request, IncludedData $includes, $response_code = 200, array $headers = [], LinkCollection $links = NULL, array $meta = []) { $links = ($links ?: new LinkCollection([])); if (!$links->hasLinkWithKey('self')) { $self_link = new Link(new CacheableMetadata(), self::getRequestLink($request), ['self']); diff --git a/core/modules/jsonapi/src/JsonApiResource/JsonApiDocumentTopLevel.php b/core/modules/jsonapi/src/JsonApiResource/JsonApiDocumentTopLevel.php index 124bc0723767e2087fc191dee294e7b21984aa32..5fc7291f9622958d0dba12a7396d6b905ea4a252 100644 --- a/core/modules/jsonapi/src/JsonApiResource/JsonApiDocumentTopLevel.php +++ b/core/modules/jsonapi/src/JsonApiResource/JsonApiDocumentTopLevel.php @@ -2,8 +2,6 @@ namespace Drupal\jsonapi\JsonApiResource; -use Drupal\Core\Field\EntityReferenceFieldItemListInterface; - /** * Represents a JSON:API document's "top level". * @@ -57,7 +55,7 @@ class JsonApiDocumentTopLevel { /** * Instantiates a JsonApiDocumentTopLevel object. * - * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifierInterface|\Drupal\jsonapi\JsonApiResource\Data|\Drupal\jsonapi\JsonApiResource\ErrorCollection|\Drupal\Core\Field\EntityReferenceFieldItemListInterface $data + * @param \Drupal\jsonapi\JsonApiResource\TopLevelDataInterface|\Drupal\jsonapi\JsonApiResource\ErrorCollection $data * The data to normalize. It can be either a ResourceObject, or a stand-in * for one, or a collection of the same. * @param \Drupal\jsonapi\JsonApiResource\IncludedData $includes @@ -69,13 +67,13 @@ class JsonApiDocumentTopLevel { * (optional) The metadata to normalize. */ public function __construct($data, IncludedData $includes, LinkCollection $links, array $meta = []) { - assert($data instanceof ResourceIdentifierInterface || $data instanceof Data || $data instanceof ErrorCollection || $data instanceof EntityReferenceFieldItemListInterface); + assert($data instanceof TopLevelDataInterface || $data instanceof ErrorCollection); assert(!$data instanceof ErrorCollection || $includes instanceof NullIncludedData); - $this->data = $data instanceof ResourceObjectData ? $data->getAccessible() : $data; - $this->includes = $includes->getAccessible(); - $this->links = $links->withContext($this); - $this->meta = $meta; - $this->omissions = $data instanceof ResourceObjectData + $this->data = $data instanceof TopLevelDataInterface ? $data->getData() : $data; + $this->includes = $includes->getData(); + $this->links = $data instanceof TopLevelDataInterface ? $data->getMergedLinks($links->withContext($this)) : $links->withContext($this); + $this->meta = $data instanceof TopLevelDataInterface ? $data->getMergedMeta($meta) : $meta; + $this->omissions = $data instanceof TopLevelDataInterface ? OmittedData::merge($data->getOmissions(), $includes->getOmissions()) : $includes->getOmissions(); } @@ -83,7 +81,7 @@ public function __construct($data, IncludedData $includes, LinkCollection $links /** * Gets the data. * - * @return \Drupal\jsonapi\JsonApiResource\ResourceObject|\Drupal\jsonapi\JsonApiResource\Data|\Drupal\jsonapi\JsonApiResource\LabelOnlyResourceObject|\Drupal\jsonapi\JsonApiResource\ErrorCollection + * @return \Drupal\jsonapi\JsonApiResource\Data|\Drupal\jsonapi\JsonApiResource\ErrorCollection * The data. */ public function getData() { diff --git a/core/modules/jsonapi/src/JsonApiResource/LinkCollection.php b/core/modules/jsonapi/src/JsonApiResource/LinkCollection.php index 4464f4e51f773d46a6591e32384b5f5782d13d51..29aac92830a01fe0e860bf406c647c6965e867ff 100644 --- a/core/modules/jsonapi/src/JsonApiResource/LinkCollection.php +++ b/core/modules/jsonapi/src/JsonApiResource/LinkCollection.php @@ -28,7 +28,7 @@ final class LinkCollection implements \IteratorAggregate { * All links objects exist within a context object. Links form a relationship * between a source IRI and target IRI. A context is the link's source. * - * @var \Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel|\Drupal\jsonapi\JsonApiResource\ResourceObject + * @var \Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel|\Drupal\jsonapi\JsonApiResource\ResourceObject|\Drupal\jsonapi\JsonApiResource\Relationship * * @see https://tools.ietf.org/html/rfc8288#section-3.2 */ @@ -39,7 +39,7 @@ final class LinkCollection implements \IteratorAggregate { * * @param \Drupal\jsonapi\JsonApiResource\Link[] $links * An associated array of key names and JSON:API Link objects. - * @param \Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel|\Drupal\jsonapi\JsonApiResource\ResourceObject $context + * @param \Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel|\Drupal\jsonapi\JsonApiResource\ResourceObject|\Drupal\jsonapi\JsonApiResource\Relationship $context * (internal use only) The context object. Use the self::withContext() * method to establish a context. This should be done automatically when * a LinkCollection is passed into a context object. @@ -51,7 +51,7 @@ public function __construct(array $links, $context = NULL) { assert(Inspector::assertAll(function ($link) { return $link instanceof Link || is_array($link) && Inspector::assertAllObjects($link, Link::class); }, $links)); - assert(is_null($context) || Inspector::assertAllObjects([$context], JsonApiDocumentTopLevel::class, ResourceObject::class)); + assert(is_null($context) || Inspector::assertAllObjects([$context], JsonApiDocumentTopLevel::class, ResourceObject::class, Relationship::class)); ksort($links); $this->links = array_map(function ($link) { return is_array($link) ? $link : [$link]; @@ -111,7 +111,7 @@ public function hasLinkWithKey($key) { /** * Establishes a new context for a LinkCollection. * - * @param \Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel|\Drupal\jsonapi\JsonApiResource\ResourceObject $context + * @param \Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel|\Drupal\jsonapi\JsonApiResource\ResourceObject|\Drupal\jsonapi\JsonApiResource\Relationship $context * The new context object. * * @return static @@ -124,7 +124,7 @@ public function withContext($context) { /** * Gets the LinkCollection's context object. * - * @return \Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel|\Drupal\jsonapi\JsonApiResource\ResourceObject + * @return \Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel|\Drupal\jsonapi\JsonApiResource\ResourceObject|\Drupal\jsonapi\JsonApiResource\Relationship * The LinkCollection's context. */ public function getContext() { diff --git a/core/modules/jsonapi/src/JsonApiResource/Relationship.php b/core/modules/jsonapi/src/JsonApiResource/Relationship.php new file mode 100644 index 0000000000000000000000000000000000000000..0f6f9a6325ec3c5853dbb1e0b941ce9cbe0a1a0d --- /dev/null +++ b/core/modules/jsonapi/src/JsonApiResource/Relationship.php @@ -0,0 +1,262 @@ +<?php + +namespace Drupal\jsonapi\JsonApiResource; + +use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\Field\EntityReferenceFieldItemListInterface; +use Drupal\Core\Url; +use Drupal\jsonapi\JsonApiSpec; +use Drupal\jsonapi\ResourceType\ResourceType; +use Drupal\jsonapi\Routing\Routes; + +/** + * Represents references from one resource object to other resource object(s). + * + * @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 Relationship implements TopLevelDataInterface { + + /** + * The context resource object of the relationship. + * + * A relationship object represents references from a resource object in + * which it’s defined to other resource objects. Respectively, the "context" + * of the relationship and the "target(s)" of the relationship. + * + * A relationship object's context either comes from the resource object that + * contains it or, in the case that the relationship object is accessed + * directly via a relationship URL, from its `self` URL, which should identify + * the resource to which it belongs. + * + * @var \Drupal\jsonapi\JsonApiResource\ResourceObject + * + * @see https://jsonapi.org/format/#document-resource-object-relationships + * @see https://jsonapi.org/recommendations/#urls-relationships + */ + protected $context; + + /** + * The data of the relationship object. + * + * @var \Drupal\jsonapi\JsonApiResource\RelationshipData + */ + protected $data; + + /** + * The relationship's public field name. + * + * @var string + */ + protected $fieldName; + + /** + * The relationship object's links. + * + * @var \Drupal\jsonapi\JsonApiResource\LinkCollection + */ + protected $links; + + /** + * The relationship object's meta member. + * + * @var array + */ + protected $meta; + + /** + * Relationship constructor. + * + * This constructor is protected by design. To create a new relationship, use + * static::createFromEntityReferenceField(). + * + * @param string $public_field_name + * The public field name of the relationship field. + * @param \Drupal\jsonapi\JsonApiResource\RelationshipData $data + * The relationship data. + * @param \Drupal\jsonapi\JsonApiResource\LinkCollection $links + * Any links for the resource object, if a `self` link is not + * provided, one will be automatically added if the resource is locatable + * and is not internal. + * @param array $meta + * Any relationship metadata. + * @param \Drupal\jsonapi\JsonApiResource\ResourceObject $context + * The relationship's context resource object. Use the + * self::withContext() method to establish a context. + * + * @see \Drupal\jsonapi\JsonApiResource\Relationship::createFromEntityReferenceField() + */ + protected function __construct($public_field_name, RelationshipData $data, LinkCollection $links, array $meta, ResourceObject $context) { + $this->fieldName = $public_field_name; + $this->data = $data; + $this->links = $links->withContext($this); + $this->meta = $meta; + $this->context = $context; + } + + /** + * Creates a new Relationship from an entity reference field. + * + * @param \Drupal\jsonapi\JsonApiResource\ResourceObject $context + * The context resource object of the relationship to be created. + * @param \Drupal\Core\Field\EntityReferenceFieldItemListInterface $field + * The entity reference field from which to create the relationship. + * @param \Drupal\jsonapi\JsonApiResource\LinkCollection $links + * (optional) Any extra links for the Relationship, if a `self` link is not + * provided, one will be automatically added if the context resource is + * locatable and is not internal. + * @param array $meta + * (optional) Any relationship metadata. + * + * @return static + * An instantiated relationship object. + */ + 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(); + return new static( + $public_field_name, + new RelationshipData(ResourceIdentifier::toResourceIdentifiers($field), $field_cardinality), + static::buildLinkCollectionFromEntityReferenceField($context, $field, $links ?: new LinkCollection([])), + $meta, + $context + ); + } + + /** + * Gets context resource object of the relationship. + * + * @return \Drupal\jsonapi\JsonApiResource\ResourceObject + * The context ResourceObject. + * + * @see \Drupal\jsonapi\JsonApiResource\Relationship::$context + */ + public function getContext() { + return $this->context; + } + + /** + * Gets the relationship object's public field name. + * + * @return string + * The relationship's field name. + */ + public function getFieldName() { + return $this->fieldName; + } + + /** + * Gets the relationship object's data. + * + * @return \Drupal\jsonapi\JsonApiResource\RelationshipData + * The relationship's data. + */ + public function getData() { + return $this->data; + } + + /** + * Gets the relationship object's links. + * + * @return \Drupal\jsonapi\JsonApiResource\LinkCollection + * The relationship object's links. + */ + public function getLinks() { + return $this->links; + } + + /** + * Gets the relationship object's metadata. + * + * @return array + * The relationship object's metadata. + */ + public function getMeta() { + return $this->meta; + } + + /** + * {@inheritdoc} + */ + public function getOmissions() { + return new OmittedData([]); + } + + /** + * {@inheritdoc} + */ + public function getMergedLinks(LinkCollection $top_level_links) { + // When directly fetching a relationship object, the relationship object's + // links become the top-level object's links unless they've been + // overridden. Overrides are especially important for the `self` link, which + // must match the link that generated the response. For example, the + // top-level `self` link might have an `include` query parameter that would + // be lost otherwise. + // See https://jsonapi.org/format/#fetching-relationships-responses-200 and + // https://jsonapi.org/format/#document-top-level. + return LinkCollection::merge($top_level_links, $this->getLinks()->filter(function ($key) use ($top_level_links) { + return !$top_level_links->hasLinkWithKey($key); + })->withContext($top_level_links->getContext())); + } + + /** + * {@inheritdoc} + */ + public function getMergedMeta(array $top_level_meta) { + return NestedArray::mergeDeep($top_level_meta, $this->getMeta()); + } + + /** + * Builds a LinkCollection for the given entity reference field. + * + * @param \Drupal\jsonapi\JsonApiResource\ResourceObject $context + * The context resource object of the relationship object. + * @param \Drupal\Core\Field\EntityReferenceFieldItemListInterface $field + * The entity reference field from which to create the links. + * @param \Drupal\jsonapi\JsonApiResource\LinkCollection $links + * Any extra links for the Relationship, if a `self` link is not provided, + * one will be automatically added if the context resource is locatable and + * is not internal. + * + * @return \Drupal\jsonapi\JsonApiResource\LinkCollection + * The built links. + */ + 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())); + if ($context_resource_type->isLocatable() && !$context_resource_type->isInternal()) { + $context_is_versionable = $context_resource_type->isVersionable(); + if (!$links->hasLinkWithKey('self')) { + $route_name = Routes::getRouteName($context_resource_type, "$public_field_name.relationship.get"); + $self_link = Url::fromRoute($route_name, ['entity' => $context->getId()]); + if ($context_is_versionable) { + $self_link->setOption('query', [JsonApiSpec::VERSION_QUERY_PARAMETER => $context->getVersionIdentifier()]); + } + $links = $links->withLink('self', new Link(new CacheableMetadata(), $self_link, ['self'])); + } + $has_non_internal_resource_type = array_reduce($context_resource_type->getRelatableResourceTypesByField($public_field_name), function ($carry, ResourceType $target) { + return $carry ?: !$target->isInternal(); + }, FALSE); + // If a `related` link was not provided, automatically generate one from + // the relationship object to the collection resource with all of the + // resources targeted by this relationship. However, that link should + // *not* be generated if all of the relatable resources are internal. + // That's because, in that case, a route will not exist for it. + if (!$links->hasLinkWithKey('related') && $has_non_internal_resource_type) { + $route_name = Routes::getRouteName($context_resource_type, "$public_field_name.related"); + $related_link = Url::fromRoute($route_name, ['entity' => $context->getId()]); + if ($context_is_versionable) { + $related_link->setOption('query', [JsonApiSpec::VERSION_QUERY_PARAMETER => $context->getVersionIdentifier()]); + } + $links = $links->withLink('related', new Link(new CacheableMetadata(), $related_link, ['related'])); + } + } + return $links; + } + +} diff --git a/core/modules/jsonapi/src/JsonApiResource/ResourceIdentifier.php b/core/modules/jsonapi/src/JsonApiResource/ResourceIdentifier.php index f129281b15d84c4f2546becf07ed0c4e2a4269b2..eb7b1de0c4b0f52511d3a803118080c7eab9256e 100644 --- a/core/modules/jsonapi/src/JsonApiResource/ResourceIdentifier.php +++ b/core/modules/jsonapi/src/JsonApiResource/ResourceIdentifier.php @@ -311,12 +311,15 @@ public static function toResourceIdentifier(EntityReferenceItem $item, $arity = */ public static function toResourceIdentifiers(EntityReferenceFieldItemListInterface $items) { $relationships = []; - foreach ($items as $item) { + foreach ($items->filterEmptyItems() as $item) { // Create a ResourceIdentifier from the field item. This will make it // comparable with all previous field items. Here, it is assumed that the // resource identifier is unique so it has no arity. If a parallel // relationship is encountered, it will be assigned later. $relationship = static::toResourceIdentifier($item); + if ($relationship->getResourceType()->isInternal()) { + continue; + } // Now, iterate over the previously seen resource identifiers in reverse // order. Reverse order is important so that when a parallel relationship // is encountered, it will have the highest arity value so the current diff --git a/core/modules/jsonapi/src/JsonApiResource/ResourceObjectData.php b/core/modules/jsonapi/src/JsonApiResource/ResourceObjectData.php index 41430f585884aca59a5e0f70d705797f57c356c8..5fadfb0356b35d2c5e3378f17d914aed4f0df429 100644 --- a/core/modules/jsonapi/src/JsonApiResource/ResourceObjectData.php +++ b/core/modules/jsonapi/src/JsonApiResource/ResourceObjectData.php @@ -14,7 +14,7 @@ * @see https://www.drupal.org/project/jsonapi/issues/3032787 * @see jsonapi.api.php */ -class ResourceObjectData extends Data { +class ResourceObjectData extends Data implements TopLevelDataInterface { /** * ResourceObjectData constructor. @@ -31,6 +31,13 @@ public function __construct($data, $cardinality = -1) { parent::__construct($data, $cardinality); } + /** + * {@inheritdoc} + */ + public function getData() { + return $this->getAccessible(); + } + /** * Gets only data to be exposed. * @@ -61,4 +68,18 @@ public function getOmissions() { return new OmittedData($omitted_data); } + /** + * {@inheritdoc} + */ + public function getMergedLinks(LinkCollection $top_level_links) { + return $top_level_links; + } + + /** + * {@inheritdoc} + */ + public function getMergedMeta(array $top_level_meta) { + return $top_level_meta; + } + } diff --git a/core/modules/jsonapi/src/JsonApiResource/TopLevelDataInterface.php b/core/modules/jsonapi/src/JsonApiResource/TopLevelDataInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..a01da15682e36a6d0b5b6a2dc68a04d586db1c6d --- /dev/null +++ b/core/modules/jsonapi/src/JsonApiResource/TopLevelDataInterface.php @@ -0,0 +1,54 @@ +<?php + +namespace Drupal\jsonapi\JsonApiResource; + +/** + * Interface for objects that can appear as top-level object data. + * + * @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 + */ +interface TopLevelDataInterface { + + /** + * Returns the data for the top-level data member of a JSON:API document. + * + * @return \Drupal\jsonapi\JsonApiResource\Data + * The top-level data. + */ + public function getData(); + + /** + * Returns the data that was omitted from the JSON:API document. + * + * @return \Drupal\jsonapi\JsonApiResource\OmittedData + * The omitted data. + */ + public function getOmissions(); + + /** + * Merges the object's links with the top-level links. + * + * @param \Drupal\jsonapi\JsonApiResource\LinkCollection $top_level_links + * The top-level links to merge. + * + * @return \Drupal\jsonapi\JsonApiResource\LinkCollection + * The merged links. + */ + public function getMergedLinks(LinkCollection $top_level_links); + + /** + * Merges the object's meta member with the top-level meta member. + * + * @param array $top_level_meta + * The top-level links to merge. + * + * @return array + * The merged meta member. + */ + public function getMergedMeta(array $top_level_meta); + +} diff --git a/core/modules/jsonapi/src/Normalizer/EntityReferenceFieldNormalizer.php b/core/modules/jsonapi/src/Normalizer/EntityReferenceFieldNormalizer.php deleted file mode 100644 index 1096de42d391ad8ef43e9294a6f06129b1138261..0000000000000000000000000000000000000000 --- a/core/modules/jsonapi/src/Normalizer/EntityReferenceFieldNormalizer.php +++ /dev/null @@ -1,120 +0,0 @@ -<?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\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. - $definition = $field->getFieldDefinition(); - $cardinality = $definition - ->getFieldStorageDefinition() - ->getCardinality(); - $resource_identifiers = array_filter(ResourceIdentifier::toResourceIdentifiers($field->filterEmptyItems()), function (ResourceIdentifierInterface $resource_identifier) { - return !$resource_identifier->getResourceType()->isInternal(); - }); - $context['field_name'] = $field->getName(); - $normalized_items = CacheableNormalization::aggregate($this->serializer->normalize($resource_identifiers, $format, $context)); - assert($context['resource_object'] instanceof ResourceObject); - $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'], $field->getName())); - $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' => $cardinality === 1 ? 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 string $relationship_field_name - * The internal relationship field name. - * - * @return array - * The relationship's links. - */ - public static function getRelationshipLinks(ResourceObject $relationship_context, $relationship_field_name) { - $resource_type = $relationship_context->getResourceType(); - if ($resource_type->isInternal() || !$resource_type->isLocatable()) { - return []; - } - $public_field_name = $resource_type->getPublicName($relationship_field_name); - $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/JsonApiDocumentTopLevelNormalizer.php b/core/modules/jsonapi/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php index 7f0dccb8670fd8e263fccc592e515533474988df..b6abaf7eaa80f2d00ec791a097d6bb8699c716fe 100644 --- a/core/modules/jsonapi/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php +++ b/core/modules/jsonapi/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php @@ -7,10 +7,8 @@ use Drupal\Component\Uuid\Uuid; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Field\EntityReferenceFieldItemListInterface; use Drupal\jsonapi\JsonApiResource\ErrorCollection; use Drupal\jsonapi\JsonApiResource\OmittedData; -use Drupal\jsonapi\JsonApiResource\ResourceObject; use Drupal\jsonapi\JsonApiSpec; use Drupal\jsonapi\Normalizer\Value\CacheableOmission; use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel; @@ -190,13 +188,7 @@ public function normalize($object, $format = NULL, array $context = []) { } else { // Add data. - // @todo: remove this if-else and just call $this->serializer->normalize($data...) in https://www.drupal.org/project/jsonapi/issues/3036285. - if ($data instanceof EntityReferenceFieldItemListInterface) { - $document['data'] = $this->normalizeEntityReferenceFieldItemList($object, $format, $context); - } - else { - $document['data'] = $this->serializer->normalize($data, $format, $context); - } + $document['data'] = $this->serializer->normalize($data, $format, $context); // Add includes. $document['included'] = $this->serializer->normalize($object->getIncludes(), $format, $context)->omitIfEmpty(); // Add omissions and metadata. @@ -240,32 +232,6 @@ protected function normalizeErrorDocument(JsonApiDocumentTopLevel $document, $fo return new CacheableNormalization($cacheability, $errors); } - /** - * Normalizes an entity reference field, i.e. a relationship document. - * - * @param \Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel $document - * The document to normalize. - * @param string $format - * The normalization format. - * @param array $context - * The normalization context. - * - * @return \Drupal\jsonapi\Normalizer\Value\CacheableNormalization - * The normalized document. - * - * @todo: remove this in https://www.drupal.org/project/jsonapi/issues/3036285. - */ - protected function normalizeEntityReferenceFieldItemList(JsonApiDocumentTopLevel $document, $format, array $context = []) { - $data = $document->getData(); - $parent_entity = $data->getEntity(); - $resource_type = $this->resourceTypeRepository->get($parent_entity->getEntityTypeId(), $parent_entity->bundle()); - $context['resource_object'] = ResourceObject::createFromEntity($resource_type, $parent_entity); - $normalized_relationship = $this->serializer->normalize($data, $format, $context); - assert($normalized_relationship instanceof CacheableNormalization); - unset($context['resource_object']); - return new CacheableNormalization($normalized_relationship, $normalized_relationship->getNormalization()['data']); - } - /** * Normalizes omitted data into a set of omission links. * diff --git a/core/modules/jsonapi/src/Normalizer/RelationshipNormalizer.php b/core/modules/jsonapi/src/Normalizer/RelationshipNormalizer.php new file mode 100644 index 0000000000000000000000000000000000000000..53354af0b6a22fc30b80d9237e8b2d3eacc8d8a5 --- /dev/null +++ b/core/modules/jsonapi/src/Normalizer/RelationshipNormalizer.php @@ -0,0 +1,32 @@ +<?php + +namespace Drupal\jsonapi\Normalizer; + +use Drupal\jsonapi\JsonApiResource\Relationship; +use Drupal\jsonapi\Normalizer\Value\CacheableNormalization; + +/** + * Normalizes a JSON:API relationship object. + * + * @internal + */ +class RelationshipNormalizer extends NormalizerBase { + + /** + * {@inheritdoc} + */ + protected $supportedInterfaceOrClass = Relationship::class; + + /** + * {@inheritdoc} + */ + public function normalize($object, $format = NULL, array $context = []) { + assert($object instanceof Relationship); + return CacheableNormalization::aggregate([ + 'data' => $this->serializer->normalize($object->getData(), $format, $context), + 'links' => $this->serializer->normalize($object->getLinks(), $format, $context)->omitIfEmpty(), + 'meta' => CacheableNormalization::permanent($object->getMeta())->omitIfEmpty(), + ]); + } + +} diff --git a/core/modules/jsonapi/src/Normalizer/ResourceObjectNormalizer.php b/core/modules/jsonapi/src/Normalizer/ResourceObjectNormalizer.php index 5fc9105b80ecf15f1adf6ad92788a790187f5fde..10a5e4cca56b8e2f91074713b0f2589cb517dfec 100644 --- a/core/modules/jsonapi/src/Normalizer/ResourceObjectNormalizer.php +++ b/core/modules/jsonapi/src/Normalizer/ResourceObjectNormalizer.php @@ -3,8 +3,10 @@ namespace Drupal\jsonapi\Normalizer; use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\Field\EntityReferenceFieldItemListInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\jsonapi\EventSubscriber\ResourceObjectNormalizationCacher; +use Drupal\jsonapi\JsonApiResource\Relationship; use Drupal\jsonapi\JsonApiResource\ResourceObject; use Drupal\jsonapi\Normalizer\Value\CacheableNormalization; use Drupal\jsonapi\Normalizer\Value\CacheableOmission; @@ -176,7 +178,17 @@ protected function serializeField($field, array $context, $format) { if (!$field_access_result->isAllowed()) { return new CacheableOmission(CacheableMetadata::createFromObject($field_access_result)); } - $normalized_field = $this->serializer->normalize($field, $format, $context); + if ($field instanceof EntityReferenceFieldItemListInterface) { + // Build the relationship object based on the entity reference and + // normalize that object instead. + assert(!empty($context['resource_object']) && $context['resource_object'] instanceof ResourceObject); + $resource_object = $context['resource_object']; + $relationship = Relationship::createFromEntityReferenceField($resource_object, $field); + $normalized_field = $this->serializer->normalize($relationship, $format, $context); + } + else { + $normalized_field = $this->serializer->normalize($field, $format, $context); + } assert($normalized_field instanceof CacheableNormalization); return $normalized_field->withCacheableDependency(CacheableMetadata::createFromObject($field_access_result)); } diff --git a/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php b/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php index 006e2b414718d59e2b4910392a32b5fe90afbbc6..a173144f2267e7439aadf50e0bf2ef5eb927ebed 100644 --- a/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php +++ b/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php @@ -1719,7 +1719,6 @@ protected function getExpectedGetRelationshipDocument($relationship_field_name, if (static::$resourceTypeIsVersionable) { assert($entity instanceof RevisionableInterface); $version_query = ['resourceVersion' => 'id:' . $entity->getRevisionId()]; - $self_link->setOption('query', $version_query); $related_link->setOption('query', $version_query); } $data = $this->getExpectedGetRelationshipDocumentData($relationship_field_name, $entity); @@ -3094,6 +3093,7 @@ public function testRevisions() { $actual_response = $this->request('GET', $relationship_url, $request_options); $expected_response = $this->getExpectedGetRelationshipResponse('field_jsonapi_test_entity_ref', $revision); $expected_document = $expected_response->getResponseData(); + $expected_document['links']['self']['href'] = $relationship_url->setAbsolute()->toString(); $expected_cacheability = $expected_response->getCacheableMetadata(); $this->assertResourceResponse(200, $expected_document, $actual_response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), FALSE, 'MISS'); // Request the related route. diff --git a/core/modules/jsonapi/tests/src/Kernel/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php b/core/modules/jsonapi/tests/src/Kernel/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php index 99bd557e41775119344eb2ade06491972934a10b..0d2f093f0189234726ca090cdc257ea38d786196 100644 --- a/core/modules/jsonapi/tests/src/Kernel/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php +++ b/core/modules/jsonapi/tests/src/Kernel/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php @@ -254,8 +254,8 @@ public function testNormalize() { 'id' => NodeType::load('article')->uuid(), ], 'links' => [ - 'self' => ['href' => Url::fromUri('internal:/jsonapi/node/article/' . $this->node->uuid() . '/relationships/node_type', ['query' => ['resourceVersion' => 'id:' . $this->node->getRevisionId()]])->setAbsolute()->toString(TRUE)->getGeneratedUrl()], 'related' => ['href' => Url::fromUri('internal:/jsonapi/node/article/' . $this->node->uuid() . '/node_type', ['query' => ['resourceVersion' => 'id:' . $this->node->getRevisionId()]])->setAbsolute()->toString(TRUE)->getGeneratedUrl()], + 'self' => ['href' => Url::fromUri('internal:/jsonapi/node/article/' . $this->node->uuid() . '/relationships/node_type', ['query' => ['resourceVersion' => 'id:' . $this->node->getRevisionId()]])->setAbsolute()->toString(TRUE)->getGeneratedUrl()], ], ], $normalized['data']['relationships']['node_type']); $this->assertTrue(!isset($normalized['data']['attributes']['created'])); diff --git a/core/modules/jsonapi/tests/src/Kernel/Normalizer/EntityReferenceFieldNormalizerTest.php b/core/modules/jsonapi/tests/src/Kernel/Normalizer/RelationshipNormalizerTest.php similarity index 94% rename from core/modules/jsonapi/tests/src/Kernel/Normalizer/EntityReferenceFieldNormalizerTest.php rename to core/modules/jsonapi/tests/src/Kernel/Normalizer/RelationshipNormalizerTest.php index 96e21ef81f4c2364df7747b21162c4d885d8396f..f77007177017f7f38afb93835057533e072f3020 100644 --- a/core/modules/jsonapi/tests/src/Kernel/Normalizer/EntityReferenceFieldNormalizerTest.php +++ b/core/modules/jsonapi/tests/src/Kernel/Normalizer/RelationshipNormalizerTest.php @@ -8,8 +8,9 @@ use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\file\Entity\File; +use Drupal\jsonapi\JsonApiResource\Relationship; use Drupal\jsonapi\JsonApiResource\ResourceObject; -use Drupal\jsonapi\Normalizer\EntityReferenceFieldNormalizer; +use Drupal\jsonapi\Normalizer\RelationshipNormalizer; use Drupal\jsonapi\Normalizer\Value\CacheableNormalization; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; @@ -17,12 +18,12 @@ use Drupal\user\Entity\User; /** - * @coversDefaultClass \Drupal\jsonapi\Normalizer\EntityReferenceFieldNormalizer + * @coversDefaultClass \Drupal\jsonapi\Normalizer\RelationshipNormalizer * @group jsonapi * * @internal */ -class EntityReferenceFieldNormalizerTest extends JsonapiKernelTestBase { +class RelationshipNormalizerTest extends JsonapiKernelTestBase { /** * {@inheritdoc} @@ -137,7 +138,7 @@ protected function setUp() { // Set up the test dependencies. $this->referencingResourceType = $this->container->get('jsonapi.resource_type.repository')->get('node', 'referencer'); - $this->normalizer = new EntityReferenceFieldNormalizer(); + $this->normalizer = new RelationshipNormalizer(); $this->normalizer->setSerializer($this->container->get('jsonapi.serializer')); } @@ -173,11 +174,10 @@ public function testNormalize($entity_property_names, $field_name, $expected) { } return $value; }, $entity_property_names); + $resource_object = ResourceObject::createFromEntity($this->referencingResourceType, $this->referencer); + $relationship = Relationship::createFromEntityReferenceField($resource_object, $resource_object->getField($field_name)); // Normalize. - $actual = $this->normalizer->normalize($this->referencer->{$field_name}, 'api_json', [ - 'account' => $this->account, - 'resource_object' => ResourceObject::createFromEntity($this->referencingResourceType, $this->referencer), - ]); + $actual = $this->normalizer->normalize($relationship, 'api_json'); // Assert. assert($actual instanceof CacheableNormalization); $this->assertEquals($expected, $actual->getNormalization());