diff --git a/jsonapi.services.yml b/jsonapi.services.yml index 200dd2b..82c99b1 100644 --- a/jsonapi.services.yml +++ b/jsonapi.services.yml @@ -76,12 +76,11 @@ services: - { name: jsonapi_normalizer_do_not_use_removal_imminent } serializer.normalizer.resource_object.jsonapi: class: Drupal\jsonapi\Normalizer\ResourceObjectNormalizer - arguments: ['@jsonapi.link_manager'] tags: - { name: jsonapi_normalizer_do_not_use_removal_imminent } serializer.normalizer.jsonapi_document_toplevel.jsonapi: class: Drupal\jsonapi\Normalizer\JsonApiDocumentTopLevelNormalizer - arguments: ['@jsonapi.link_manager', '@entity_type.manager', '@jsonapi.resource_type.repository'] + arguments: ['@entity_type.manager', '@jsonapi.resource_type.repository'] tags: - { name: jsonapi_normalizer_do_not_use_removal_imminent } serializer.normalizer.link_collection.jsonapi: @@ -193,7 +192,6 @@ services: - '@entity_field.manager' - '@jsonapi.file.uploader.field' - '@http_kernel' - - '@jsonapi.link_manager' # Event subscribers. jsonapi.custom_query_parameter_names_validator.subscriber: diff --git a/src/Controller/EntityResource.php b/src/Controller/EntityResource.php index 740c64e..ed276b4 100644 --- a/src/Controller/EntityResource.php +++ b/src/Controller/EntityResource.php @@ -220,14 +220,10 @@ class EntityResource { // According to JSON:API specification, when a new entity was created // we should send "Location" header to the frontend. - $entity_url = $this->linkManager->getEntityLink( - $parsed_entity->uuid(), - $resource_type, - [], - 'individual' - ); - if ($entity_url) { - $response->headers->set('Location', $entity_url); + if ($resource_type->isLocatable()) { + $url = $resource_object->toUrl()->setAbsolute()->toString(TRUE); + $response->addCacheableDependency($url); + $response->headers->set('Location', $url->getGeneratedUrl()); } // Return response object with updated headers info. diff --git a/src/Controller/FileUpload.php b/src/Controller/FileUpload.php index 9f0d02a..0fbc0c8 100644 --- a/src/Controller/FileUpload.php +++ b/src/Controller/FileUpload.php @@ -18,7 +18,6 @@ use Drupal\jsonapi\JsonApiResource\LinkCollection; use Drupal\jsonapi\JsonApiResource\NullEntityCollection; use Drupal\jsonapi\JsonApiResource\ResourceObject; use Drupal\jsonapi\ResourceResponse; -use Drupal\jsonapi\LinkManager\LinkManager; use Drupal\jsonapi\ResourceType\ResourceType; use Drupal\jsonapi\ForwardCompatibility\FileFieldUploader; use Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException; @@ -66,13 +65,6 @@ class FileUpload { */ protected $httpKernel; - /** - * The link manager service. - * - * @var \Drupal\jsonapi\LinkManager\LinkManager - */ - protected $linkManager; - /** * Creates a new FileUpload instance. * @@ -84,15 +76,12 @@ class FileUpload { * The file uploader. * @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel * An HTTP kernel for making subrequests. - * @param \Drupal\jsonapi\LinkManager\LinkManager $link_manager - * The link manager service. */ - public function __construct(AccountInterface $current_user, EntityFieldManagerInterface $field_manager, FileFieldUploader $file_uploader, HttpKernelInterface $http_kernel, LinkManager $link_manager) { + public function __construct(AccountInterface $current_user, EntityFieldManagerInterface $field_manager, FileFieldUploader $file_uploader, HttpKernelInterface $http_kernel) { $this->currentUser = $current_user; $this->fieldManager = $field_manager; $this->fileUploader = $file_uploader; $this->httpKernel = $http_kernel; - $this->linkManager = $link_manager; } /** diff --git a/src/JsonApiResource/LinkCollection.php b/src/JsonApiResource/LinkCollection.php index b248a7e..bc5fb24 100644 --- a/src/JsonApiResource/LinkCollection.php +++ b/src/JsonApiResource/LinkCollection.php @@ -35,18 +35,19 @@ 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 $context + * @param \Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel|\Drupal\jsonapi\JsonApiResource\ResourceObject $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. */ - public function __construct(array $links, JsonApiDocumentTopLevel $context = NULL) { + public function __construct(array $links, $context = NULL) { assert(Inspector::assertAll(function ($key) { return static::validKey($key); }, array_keys($links))); 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)); ksort($links); $this->links = array_map(function ($link) { return is_array($link) ? $link : [$link]; @@ -90,16 +91,29 @@ final class LinkCollection implements \IteratorAggregate { return new static($merged, $this->context); } + /** + * Whether a link with the given key exists. + * + * @param string $key + * The key. + * + * @return bool + * TRUE if a link with the given key exist, FALSE otherwise. + */ + public function hasLinkWithKey($key) { + return array_key_exists($key, $this->links); + } + /** * Establishes a new context for a LinkCollection. * - * @param \Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel $context + * @param \Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel|\Drupal\jsonapi\JsonApiResource\ResourceObject $context * The new context object. * * @return static * A new LinkCollection with the given context. */ - public function withContext(JsonApiDocumentTopLevel $context) { + public function withContext($context) { return new static($this->links, $context); } diff --git a/src/JsonApiResource/ResourceObject.php b/src/JsonApiResource/ResourceObject.php index 4474898..e9d41e3 100644 --- a/src/JsonApiResource/ResourceObject.php +++ b/src/JsonApiResource/ResourceObject.php @@ -4,11 +4,14 @@ namespace Drupal\jsonapi\JsonApiResource; use Drupal\Core\Cache\CacheableDependencyInterface; use Drupal\Core\Cache\CacheableDependencyTrait; +use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Config\Entity\ConfigEntityInterface; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper; +use Drupal\Core\Url; use Drupal\jsonapi\ResourceType\ResourceType; +use Drupal\jsonapi\Routing\Routes; /** * Represent a JSON:API resource object. @@ -49,6 +52,13 @@ class ResourceObject implements CacheableDependencyInterface, ResourceIdentifier */ protected $entity; + /** + * Resource object level links. + * + * @var \Drupal\jsonapi\JsonApiResource\LinkCollection + */ + protected $links; + /** * ResourceObject constructor. * @@ -56,13 +66,21 @@ class ResourceObject implements CacheableDependencyInterface, ResourceIdentifier * The JSON:API resource type of the resource object. * @param \Drupal\Core\Entity\EntityInterface $entity * The entity to be represented by this resource object. + * @param \Drupal\jsonapi\JsonApiResource\LinkCollection $links + * (optional) Any links for the resource object, if a `self` link is not + * provided, one will be automatically added if the resource is locatable. */ - public function __construct(ResourceType $resource_type, EntityInterface $entity) { + public function __construct(ResourceType $resource_type, EntityInterface $entity, LinkCollection $links = NULL) { + $this->setCacheability($entity); $this->resourceType = $resource_type; $this->entity = $entity; $this->fields = $this->extractFields($entity); $this->resourceIdentifier = new ResourceIdentifier($resource_type, $this->entity->uuid()); - $this->setCacheability($entity); + $this->links = is_null($links) ? (new LinkCollection([]))->withContext($this) : $links->withContext($this); + if ($resource_type->isLocatable() && !$this->links->hasLinkWithKey('self')) { + $self_link = Url::fromRoute(Routes::getRouteName($this->getResourceType(), 'individual'), ['entity' => $this->getId()]); + $this->links = $this->links->withLink('self', new Link(new CacheableMetadata(), $self_link, ['self'])); + } } /** @@ -105,6 +123,36 @@ class ResourceObject implements CacheableDependencyInterface, ResourceIdentifier return $this->fields; } + /** + * Gets the ResourceObject's links. + * + * @return \Drupal\jsonapi\JsonApiResource\LinkCollection + * The resource object's links. + */ + public function getLinks() { + return $this->links; + } + + /** + * Gets a Url for the ResourceObject. + * + * @return \Drupal\Core\Url + * The URL for the identified resource object. + * + * @throws \LogicException + * Thrown if the resource object is not locatable. + * + * @see \Drupal\jsonapi\ResourceType\ResourceTypeRepository::isLocatableResourceType() + */ + public function toUrl() { + foreach ($this->links as $key => $link) { + if ($key === 'self') { + return $link->getUri(); + } + } + throw new \LogicException('A Url does not exist for this resource object because its resource type is not locatable.'); + } + /** * Extracts the entity's fields. * diff --git a/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php b/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php index 99847b9..5e336c9 100644 --- a/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php +++ b/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php @@ -15,7 +15,6 @@ use Drupal\jsonapi\JsonApiResource\ResourceObject; use Drupal\jsonapi\JsonApiSpec; use Drupal\jsonapi\Normalizer\Value\HttpExceptionNormalizerValue; use Drupal\jsonapi\JsonApiResource\EntityCollection; -use Drupal\jsonapi\LinkManager\LinkManager; use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel; use Drupal\jsonapi\Normalizer\Value\CacheableNormalization; use Drupal\jsonapi\ResourceType\ResourceType; @@ -41,13 +40,6 @@ class JsonApiDocumentTopLevelNormalizer extends NormalizerBase implements Denorm */ protected $supportedInterfaceOrClass = JsonApiDocumentTopLevel::class; - /** - * The link manager to get the links. - * - * @var \Drupal\jsonapi\LinkManager\LinkManager - */ - protected $linkManager; - /** * The entity type manager. * @@ -65,15 +57,12 @@ class JsonApiDocumentTopLevelNormalizer extends NormalizerBase implements Denorm /** * Constructs a JsonApiDocumentTopLevelNormalizer object. * - * @param \Drupal\jsonapi\LinkManager\LinkManager $link_manager - * The link manager to get the links. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. * @param \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository * The JSON:API resource type repository. */ - public function __construct(LinkManager $link_manager, EntityTypeManagerInterface $entity_type_manager, ResourceTypeRepositoryInterface $resource_type_repository) { - $this->linkManager = $link_manager; + public function __construct(EntityTypeManagerInterface $entity_type_manager, ResourceTypeRepositoryInterface $resource_type_repository) { $this->entityTypeManager = $entity_type_manager; $this->resourceTypeRepository = $resource_type_repository; } diff --git a/src/Normalizer/ResourceObjectNormalizer.php b/src/Normalizer/ResourceObjectNormalizer.php index 6a2f566..d7a6eb2 100644 --- a/src/Normalizer/ResourceObjectNormalizer.php +++ b/src/Normalizer/ResourceObjectNormalizer.php @@ -8,7 +8,6 @@ use Drupal\jsonapi\JsonApiResource\ResourceObject; use Drupal\jsonapi\LabelOnlyResourceObject; use Drupal\jsonapi\Normalizer\Value\CacheableNormalization; use Drupal\jsonapi\Normalizer\Value\CacheableOmission; -use Drupal\jsonapi\LinkManager\LinkManager; /** * Converts the JSON:API module ResourceObject into a JSON:API array structure. @@ -25,23 +24,6 @@ class ResourceObjectNormalizer extends NormalizerBase { LabelOnlyResourceObject::class, ]; - /** - * The link manager. - * - * @var \Drupal\jsonapi\LinkManager\LinkManager - */ - protected $linkManager; - - /** - * Constructs an ResourceObjectNormalizer object. - * - * @param \Drupal\jsonapi\LinkManager\LinkManager $link_manager - * The link manager. - */ - public function __construct(LinkManager $link_manager) { - $this->linkManager = $link_manager; - } - /** * {@inheritdoc} */ @@ -81,18 +63,15 @@ class ResourceObjectNormalizer extends NormalizerBase { 'type' => $resource_type->getTypeName(), 'id' => $object->getId(), ]; - $normalized['links']['self']['href'] = $this->linkManager->getEntityLink( - $normalized['id'], - $resource_type, - [], - 'individual' - ); + $links = $this->serializer->normalize($object->getLinks(), $format, $context); + assert($links instanceof CacheableNormalization); + $normalized['links'] = $links->getNormalization(); $relationship_field_names = array_keys($resource_type->getRelatableResourceTypes()); $attributes = CacheableNormalization::aggregate(array_diff_key($normalizer_values, array_flip($relationship_field_names))); $relationships = CacheableNormalization::aggregate(array_intersect_key($normalizer_values, array_flip($relationship_field_names))); $normalized['attributes'] = $attributes->getNormalization(); $normalized['relationships'] = $relationships->getNormalization(); - return (new CacheableNormalization($object, array_filter($normalized)))->withCacheableDependency($attributes)->withCacheableDependency($relationships); + return (new CacheableNormalization($object, array_filter($normalized)))->withCacheableDependency($attributes)->withCacheableDependency($relationships)->withCacheableDependency($links); } /** diff --git a/src/Routing/Routes.php b/src/Routing/Routes.php index 3ccfd29..9236405 100644 --- a/src/Routing/Routes.php +++ b/src/Routing/Routes.php @@ -407,7 +407,7 @@ class Routes implements ContainerInjectionInterface { * @return string * The generated route name. */ - protected static function getRouteName(ResourceType $resource_type, $route_type) { + public static function getRouteName(ResourceType $resource_type, $route_type) { return sprintf('jsonapi.%s.%s', $resource_type->getTypeName(), $route_type); } diff --git a/tests/src/Unit/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php b/tests/src/Unit/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php index 20dcfde..a30b1f6 100644 --- a/tests/src/Unit/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php +++ b/tests/src/Unit/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php @@ -10,7 +10,6 @@ use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\jsonapi\Context\FieldResolver; use Drupal\jsonapi\ResourceType\ResourceType; use Drupal\jsonapi\Normalizer\JsonApiDocumentTopLevelNormalizer; -use Drupal\jsonapi\LinkManager\LinkManager; use Drupal\Tests\UnitTestCase; use Prophecy\Argument; use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; @@ -37,7 +36,6 @@ class JsonApiDocumentTopLevelNormalizerTest extends UnitTestCase { * {@inheritdoc} */ public function setUp() { - $link_manager = $this->prophesize(LinkManager::class); $resource_type_repository = $this->prophesize(ResourceTypeRepository::class); $field_resolver = $this->prophesize(FieldResolver::class); @@ -69,7 +67,6 @@ class JsonApiDocumentTopLevelNormalizerTest extends UnitTestCase { $entity_type_manager->getDefinition('node')->willReturn($entity_type->reveal()); $this->normalizer = new JsonApiDocumentTopLevelNormalizer( - $link_manager->reveal(), $entity_type_manager->reveal(), $resource_type_repository->reveal(), $field_resolver->reveal()