src/Controller/EntityResource.php | 16 ++++- src/EventSubscriber/ResourceResponseSubscriber.php | 5 +- src/JsonApiResource/JsonApiDocumentTopLevel.php | 30 ++++++++- .../JsonApiDocumentTopLevelNormalizer.php | 20 +----- src/Normalizer/RelationshipItemNormalizer.php | 2 +- .../JsonApiDocumentTopLevelNormalizerValue.php | 71 ++++++---------------- tests/src/Functional/ResourceTestBase.php | 8 +-- .../JsonApiDocumentTopLevelNormalizerTest.php | 24 ++------ 8 files changed, 73 insertions(+), 103 deletions(-) diff --git a/src/Controller/EntityResource.php b/src/Controller/EntityResource.php index 3b365e5..94ffb31 100644 --- a/src/Controller/EntityResource.php +++ b/src/Controller/EntityResource.php @@ -801,12 +801,15 @@ class EntityResource { * The response code. * @param array $headers * An array of response headers. + * @param \Drupal\Core\Url[]|string[] $links + * The URLs to which to link. A 'self' link is added automatically. * * @return \Drupal\jsonapi\ResourceResponse * The response. */ - protected function buildWrappedResponse($data, $response_code = 200, array $headers = []) { - return new ResourceResponse(new JsonApiDocumentTopLevel($data), $response_code, $headers); + protected function buildWrappedResponse($data, $response_code = 200, array $headers = [], array $links = []) { + $links['self'] = $this->linkManager->getRequestLink(\Drupal::request()); + return new ResourceResponse(new JsonApiDocumentTopLevel($data, $links), $response_code, $headers); } /** @@ -821,7 +824,14 @@ class EntityResource { * The response. */ protected function respondWithCollection(EntityCollection $entity_collection, $entity_type_id) { - $response = $this->buildWrappedResponse($entity_collection); + $link_context = [ + 'has_next_page' => $entity_collection->hasNextPage(), + 'total_count' => $this->resourceType->includeCount() + ? $entity_collection->getTotalCount() + : FALSE + ]; + $collection_links = $this->linkManager->getPagerLinks(\Drupal::request(), $link_context); + $response = $this->buildWrappedResponse($entity_collection, 200, [], $collection_links); // When a new change to any entity in the resource happens, we cannot ensure // the validity of this cached list. Add the list tag to deal with that. diff --git a/src/EventSubscriber/ResourceResponseSubscriber.php b/src/EventSubscriber/ResourceResponseSubscriber.php index 6a2e825..e54373a 100644 --- a/src/EventSubscriber/ResourceResponseSubscriber.php +++ b/src/EventSubscriber/ResourceResponseSubscriber.php @@ -126,10 +126,10 @@ class ResourceResponseSubscriber implements EventSubscriberInterface { } /** - * Generates the context to pass to normalizers. + * Generates a top-level JSON API normalization context. * * @param \Symfony\Component\HttpFoundation\Request $request - * The request to get the URL params from to expand the context. + * The request from which the context can be derived. * * @return array * The generated context. @@ -158,7 +158,6 @@ class ResourceResponseSubscriber implements EventSubscriberInterface { $public_includes = array_reduce($public_includes, 'array_merge', []); // Build the expanded context. $context = [ - 'request' => $request, 'account' => NULL, 'sparse_fieldset' => NULL, 'resource_type' => $resource_type, diff --git a/src/JsonApiResource/JsonApiDocumentTopLevel.php b/src/JsonApiResource/JsonApiDocumentTopLevel.php index bb43cf7..dc98c7e 100644 --- a/src/JsonApiResource/JsonApiDocumentTopLevel.php +++ b/src/JsonApiResource/JsonApiDocumentTopLevel.php @@ -2,6 +2,9 @@ namespace Drupal\jsonapi\JsonApiResource; +use Drupal\Component\Assertion\Inspector; +use Drupal\Core\Url; + /** * Represents a JSON API document's "top level". * @@ -10,7 +13,7 @@ namespace Drupal\jsonapi\JsonApiResource; * @internal * * @todo Add the missing required members: 'error' and 'meta' or document why not. - * @todo Add support for the missing optional members: 'jsonapi', 'links' and 'included' or document why not. + * @todo Add support for the missing optional members: 'jsonapi' and 'included' or document why not. */ class JsonApiDocumentTopLevel { @@ -22,14 +25,27 @@ class JsonApiDocumentTopLevel { protected $data; /** + * @var array|\Drupal\Core\Url[]|string[] + */ + protected $links; + + /** * Instantiates a JsonApiDocumentTopLevel object. * * @param \Drupal\Core\Entity\EntityInterface|\Drupal\jsonapi\EntityCollection|\Drupal\jsonapi\LabelOnlyEntity $data * The data to normalize. It can be either a straight up entity or a * collection of entities. + * @param \Drupal\Core\Url[]|string[] $links + * The URLs to which to link. */ - public function __construct($data) { + public function __construct($data, array $links) { + assert(empty(array_diff(array_keys($links), ['self', 'first', 'next', 'prev', 'last', 'related']))); + assert(Inspector::assertAll(function ($v) { + return is_string($v) || $v instanceof Url; + }, $links)); + $this->data = $data; + $this->links = $links; } /** @@ -42,4 +58,14 @@ class JsonApiDocumentTopLevel { return $this->data; } + /** + * Gets the links. + * + * @return \Drupal\Core\Url[] $links + * The document-level links. + */ + public function getLinks() { + return $this->links; + } + } diff --git a/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php b/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php index 442fafc..d693468 100644 --- a/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php +++ b/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php @@ -171,33 +171,17 @@ class JsonApiDocumentTopLevelNormalizer extends NormalizerBase implements Denorm $normalizer_values = [ $this->serializer->normalize($data, $format, $context), ]; - $link_context = ['link_manager' => $this->linkManager]; - return new JsonApiDocumentTopLevelNormalizerValue($normalizer_values, $context, $link_context, FALSE); + return new JsonApiDocumentTopLevelNormalizerValue($normalizer_values, [], FALSE); } $is_collection = $data instanceof EntityCollection; - $include_count = $context['resource_type']->includeCount(); // To improve the logical workflow deal with an array at all times. $entities = $is_collection ? $data->toArray() : [$data]; - $context['has_next_page'] = $is_collection ? $data->hasNextPage() : FALSE; - - if ($include_count) { - $context['total_count'] = $is_collection ? $data->getTotalCount() : 1; - } $serializer = $this->serializer; $normalizer_values = array_map(function ($entity) use ($format, $context, $serializer) { return $serializer->normalize($entity, $format, $context); }, $entities); - $link_context = [ - 'link_manager' => $this->linkManager, - 'has_next_page' => $context['has_next_page'], - ]; - - if ($include_count) { - $link_context['total_count'] = $context['total_count']; - } - - return new JsonApiDocumentTopLevelNormalizerValue($normalizer_values, $context, $link_context, $is_collection); + return new JsonApiDocumentTopLevelNormalizerValue($normalizer_values, $object->getLinks(), $is_collection); } /** diff --git a/src/Normalizer/RelationshipItemNormalizer.php b/src/Normalizer/RelationshipItemNormalizer.php index 0cb5b9d..f8ca9f7 100644 --- a/src/Normalizer/RelationshipItemNormalizer.php +++ b/src/Normalizer/RelationshipItemNormalizer.php @@ -60,7 +60,7 @@ class RelationshipItemNormalizer extends FieldItemNormalizer { if (!empty($context['include']) && in_array($host_field_name, $context['include']) && $target_entity !== NULL) { $context = $this->buildSubContext($context, $target_entity, $host_field_name); $entity_and_access = EntityResource::getEntityAndAccess($target_entity); - $included_normalizer_value = $this->serializer->normalize(new JsonApiDocumentTopLevel($entity_and_access['entity']), $format, $context); + $included_normalizer_value = $this->serializer->normalize(new JsonApiDocumentTopLevel($entity_and_access['entity'], []), $format, $context); } else { $included_normalizer_value = NULL; diff --git a/src/Normalizer/Value/JsonApiDocumentTopLevelNormalizerValue.php b/src/Normalizer/Value/JsonApiDocumentTopLevelNormalizerValue.php index 74b6527..5bbaaad 100644 --- a/src/Normalizer/Value/JsonApiDocumentTopLevelNormalizerValue.php +++ b/src/Normalizer/Value/JsonApiDocumentTopLevelNormalizerValue.php @@ -2,9 +2,11 @@ namespace Drupal\jsonapi\Normalizer\Value; +use Drupal\Component\Assertion\Inspector; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Cache\RefinableCacheableDependencyInterface; use Drupal\Core\Cache\RefinableCacheableDependencyTrait; +use Drupal\Core\Url; use Drupal\jsonapi\JsonApiSpec; /** @@ -24,18 +26,16 @@ class JsonApiDocumentTopLevelNormalizerValue implements ValueExtractorInterface, protected $values; /** - * The includes. - * - * @var array + * @var array|\Drupal\Core\Url[]|string[] */ - protected $includes; + protected $links; /** - * The resource path. + * The includes. * * @var array */ - protected $context; + protected $includes; /** * Is collection? @@ -45,34 +45,17 @@ class JsonApiDocumentTopLevelNormalizerValue implements ValueExtractorInterface, protected $isCollection; /** - * The link manager. - * - * @var \Drupal\jsonapi\LinkManager\LinkManager - */ - protected $linkManager; - - /** - * The link context. - * - * @var array - */ - protected $linkContext; - - /** * Instantiates a JsonApiDocumentTopLevelNormalizerValue object. * * @param \Drupal\Core\Entity\EntityInterface[] $values * The data to normalize. It can be either a straight up entity or a * collection of entities. - * @param array $context - * The context. - * @param array $link_context - * All the objects and variables needed to generate the links for this - * relationship. + * @param \Drupal\Core\Url[]|string[] $links + * The URLs to which to link. * @param bool $is_collection * TRUE if this is a serialization for a list. */ - public function __construct(array $values, array $context, array $link_context, $is_collection = FALSE) { + public function __construct(array $values, array $links, $is_collection = FALSE) { $this->values = $values; array_walk($values, [$this, 'addCacheableDependency']); // Make sure that different sparse fieldsets are cached differently. @@ -82,12 +65,14 @@ class JsonApiDocumentTopLevelNormalizerValue implements ValueExtractorInterface, // Every JSON API document contains absolute URLs. $this->addCacheContexts(['url.site']); - $this->context = $context; $this->isCollection = $is_collection; - $this->linkManager = $link_context['link_manager']; - // Remove the manager and store the link context. - unset($link_context['link_manager']); - $this->linkContext = $link_context; + + assert(empty(array_diff(array_keys($links), ['self', 'first', 'next', 'prev', 'last', 'related']))); + assert(Inspector::assertAll(function ($v) { + return is_string($v) || $v instanceof Url; + }, $links)); + $this->links = $links; + // Get an array of arrays of includes. $this->includes = array_map(function ($value) { return $value->getIncludes(); @@ -114,7 +99,7 @@ class JsonApiDocumentTopLevelNormalizerValue implements ValueExtractorInterface, 'links' => ['self' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK], ], ], - 'links' => [], + 'links' => $this->links, ]; foreach ($this->values as $normalizer_value) { @@ -139,28 +124,6 @@ class JsonApiDocumentTopLevelNormalizerValue implements ValueExtractorInterface, array_filter($rasterized['data']) : reset($rasterized['data']); - // Add the self link. - if ($this->context['request']) { - /* @var \Symfony\Component\HttpFoundation\Request $request */ - $request = $this->context['request']; - $rasterized['links'] += [ - 'self' => $this->linkManager->getRequestLink($request), - ]; - // If this is a collection we need to append the pager data. - if ($this->isCollection) { - // Add the pager links. - $rasterized['links'] += $this->linkManager->getPagerLinks($request, $this->linkContext); - - // Add the pre-calculated total count to the meta section. - if (isset($this->context['total_count'])) { - $rasterized = NestedArray::mergeDeepArray([ - $rasterized, - ['meta' => ['count' => $this->context['total_count']]], - ]); - } - } - } - // This is the top-level JSON API document, therefore the rasterized value // must include the rasterized includes: there is no further level to bubble // them to! diff --git a/tests/src/Functional/ResourceTestBase.php b/tests/src/Functional/ResourceTestBase.php index 82b1ca3..b5feb6f 100644 --- a/tests/src/Functional/ResourceTestBase.php +++ b/tests/src/Functional/ResourceTestBase.php @@ -322,11 +322,11 @@ abstract class ResourceTestBase extends BrowserTestBase { * The JSON API normalization for the given entity. */ protected function normalize(EntityInterface $entity, Url $url) { - return $this->serializer->normalize(new JsonApiDocumentTopLevel($entity), 'api_json', [ + $doc = new JsonApiDocumentTopLevel($entity, [ + 'self' => $url->toString(TRUE)->getGeneratedUrl(), + ]); + return $this->serializer->normalize($doc, 'api_json', [ 'resource_type' => $this->container->get('jsonapi.resource_type.repository')->getByTypeName(static::$resourceTypeName), - // Pass a Request object to the normalizer; this will be considered the - // "current request" for generating the "self" link. - 'request' => Request::create($url->toString(TRUE)->getGeneratedUrl()), 'account' => $this->account, ])->rasterizeValue(); } diff --git a/tests/src/Kernel/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php b/tests/src/Kernel/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php index 192c1f4..a342e5e 100644 --- a/tests/src/Kernel/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php +++ b/tests/src/Kernel/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php @@ -209,10 +209,9 @@ class JsonApiDocumentTopLevelNormalizerTest extends JsonapiKernelTestBase { $jsonapi_doc_object = $this ->getNormalizer() ->normalize( - new JsonApiDocumentTopLevel($this->node), + new JsonApiDocumentTopLevel($this->node, []), 'api_json', [ - 'request' => $request, 'resource_type' => $resource_type, 'account' => NULL, 'sparse_fieldset' => [ @@ -315,7 +314,6 @@ class JsonApiDocumentTopLevelNormalizerTest extends JsonapiKernelTestBase { $document_wrapper->reveal(), 'api_json', [ - 'request' => $request, 'resource_type' => $resource_type, 'account' => NULL, ] @@ -335,16 +333,14 @@ class JsonApiDocumentTopLevelNormalizerTest extends JsonapiKernelTestBase { */ public function testNormalizeUuid() { list($request, $resource_type) = $this->generateProphecies('node', 'article', 'uuid'); - $document_wrapper = $this->prophesize(JsonApiDocumentTopLevel::class); - $document_wrapper->getData()->willReturn($this->node); + $document_wrapper = new JsonApiDocumentTopLevel($this->node, []); $jsonapi_doc_object = $this ->getNormalizer() ->normalize( - $document_wrapper->reveal(), + $document_wrapper, 'api_json', [ - 'request' => $request, 'resource_type' => $resource_type, 'account' => NULL, 'include' => [ @@ -392,7 +388,6 @@ class JsonApiDocumentTopLevelNormalizerTest extends JsonapiKernelTestBase { new BadRequestHttpException('Lorem'), 'api_json', [ - 'request' => $request, 'resource_type' => $resource_type, 'data_wrapper' => 'errors', ] @@ -410,13 +405,11 @@ class JsonApiDocumentTopLevelNormalizerTest extends JsonapiKernelTestBase { */ public function testNormalizeConfig() { list($request, $resource_type) = $this->generateProphecies('node_type', 'node_type', 'id'); - $document_wrapper = $this->prophesize(JsonApiDocumentTopLevel::class); - $document_wrapper->getData()->willReturn($this->nodeType); + $document_wrapper = new JsonApiDocumentTopLevel($this->nodeType, []); $jsonapi_doc_object = $this ->getNormalizer() - ->normalize($document_wrapper->reveal(), 'api_json', [ - 'request' => $request, + ->normalize($document_wrapper, 'api_json', [ 'resource_type' => $resource_type, 'account' => NULL, 'sparse_fieldset' => [ @@ -449,7 +442,6 @@ class JsonApiDocumentTopLevelNormalizerTest extends JsonapiKernelTestBase { $node = $this ->getNormalizer() ->denormalize(Json::decode($payload), NULL, 'api_json', [ - 'request' => $request, 'resource_type' => $resource_type, ]); $this->assertInstanceOf('\Drupal\node\Entity\Node', $node); @@ -518,7 +510,6 @@ class JsonApiDocumentTopLevelNormalizerTest extends JsonapiKernelTestBase { $node = $this ->getNormalizer() ->denormalize(Json::decode($payload), NULL, 'api_json', [ - 'request' => $request, 'resource_type' => $resource_type, ]); @@ -583,7 +574,6 @@ class JsonApiDocumentTopLevelNormalizerTest extends JsonapiKernelTestBase { $this ->getNormalizer() ->denormalize(Json::decode($payload), NULL, 'api_json', [ - 'request' => $request, 'resource_type' => $resource_type, ]); @@ -602,7 +592,6 @@ class JsonApiDocumentTopLevelNormalizerTest extends JsonapiKernelTestBase { try { $this->container->get('jsonapi_test_normalizers_kernel.jsonapi_document_toplevel') ->denormalize(Json::decode($payload), NULL, 'api_json', [ - 'request' => $request, 'resource_type' => $resource_type, ]); @@ -683,11 +672,10 @@ class JsonApiDocumentTopLevelNormalizerTest extends JsonapiKernelTestBase { public function testCacheableMetadata(CacheableMetadata $expected_metadata, $fields = NULL, $includes = NULL) { list($request, $resource_type) = $this->generateProphecies('node', 'article'); $context = [ - 'request' => $this->decorateRequest($request, $fields, $includes), 'resource_type' => $resource_type, 'account' => NULL, ]; - $jsonapi_doc_object = $this->getNormalizer()->normalize(new JsonApiDocumentTopLevel($this->node), 'api_json', $context); + $jsonapi_doc_object = $this->getNormalizer()->normalize(new JsonApiDocumentTopLevel($this->node, []), 'api_json', $context); $this->assertArraySubset($expected_metadata->getCacheTags(), $jsonapi_doc_object->getCacheTags()); $this->assertArraySubset($expected_metadata->getCacheContexts(), $jsonapi_doc_object->getCacheContexts()); $this->assertSame($expected_metadata->getCacheMaxAge(), $jsonapi_doc_object->getCacheMaxAge());