From 008b5ca604eded962cef31c59f13f480582176b0 Mon Sep 17 00:00:00 2001 From: wimleers <wimleers@99777.no-reply.drupal.org> Date: Mon, 18 Feb 2019 16:01:01 +0100 Subject: [PATCH] Issue #3032451 by Wim Leers, gabesullice, e0ipso: Compatibility with upcoming JSON:API 2.2 release: JSON:API Extras 3.4 --- jsonapi_extras.services.yml | 30 +++-- .../src/Controller/EntityResource.php | 70 +++++++++++ .../JsonApiDefaultsJsonApiParamEnhancer.php | 112 ----------------- .../src/JsonapiDefaultsServiceProvider.php | 5 - src/EntityToJsonApi.php | 117 ++++-------------- src/JsonapiExtrasServiceProvider.php | 16 ++- src/Normalizer/ConfigEntityDenormalizer.php | 28 +++++ src/Normalizer/ConfigEntityNormalizer.php | 48 ------- ...rait.php => ContentEntityDenormalizer.php} | 65 +++++----- src/Normalizer/ContentEntityNormalizer.php | 28 ----- src/Normalizer/FieldItemNormalizer.php | 2 +- src/Normalizer/RelationshipItemNormalizer.php | 29 +++-- src/Normalizer/ResourceObjectNormalizer.php | 71 +++++++++++ .../Kernel/Controller/EntityResourceTest.php | 59 +++++---- 14 files changed, 317 insertions(+), 363 deletions(-) delete mode 100644 modules/jsonapi_defaults/src/JsonApiDefaultsJsonApiParamEnhancer.php create mode 100644 src/Normalizer/ConfigEntityDenormalizer.php delete mode 100644 src/Normalizer/ConfigEntityNormalizer.php rename src/Normalizer/{EntityNormalizerTrait.php => ContentEntityDenormalizer.php} (67%) delete mode 100644 src/Normalizer/ContentEntityNormalizer.php create mode 100644 src/Normalizer/ResourceObjectNormalizer.php diff --git a/jsonapi_extras.services.yml b/jsonapi_extras.services.yml index 4dcd03f..101c16b 100644 --- a/jsonapi_extras.services.yml +++ b/jsonapi_extras.services.yml @@ -11,21 +11,27 @@ services: serializer.normalizer.entity_reference_item.jsonapi_extras: class: Drupal\jsonapi_extras\Normalizer\RelationshipItemNormalizer arguments: - - '@serializer.normalizer.entity_reference_item.jsonapi' + - '@serializer.normalizer.resource_identifier.jsonapi' - '@jsonapi.resource_type.repository' tags: - { name: normalizer, priority: 1025, format: api_json } - serializer.normalizer.entity.jsonapi_extras: - class: Drupal\jsonapi_extras\Normalizer\ContentEntityNormalizer - parent: serializer.normalizer.entity.jsonapi - calls: - - [setSerializer, ['@jsonapi.serializer_do_not_use_removal_imminent']] + serializer.normalizer.resource_object.jsonapi_extras: + class: Drupal\jsonapi_extras\Normalizer\ResourceObjectNormalizer + decorates: serializer.normalizer.resource_object.jsonapi + arguments: ['@serializer.normalizer.resource_object.jsonapi_extras.inner'] + tags: + - { name: normalizer, 1022, format: api_json } + serializer.normalizer.content_entity.jsonapi_extras: + class: Drupal\jsonapi_extras\Normalizer\ContentEntityDenormalizer + decorates: serializer.normalizer.content_entity.jsonapi + arguments: ['@serializer.normalizer.content_entity.jsonapi_extras.inner'] tags: - { name: normalizer, priority: 1022, format: api_json } serializer.normalizer.config_entity.jsonapi_extras: - class: Drupal\jsonapi_extras\Normalizer\ConfigEntityNormalizer - parent: serializer.normalizer.config_entity.jsonapi + class: Drupal\jsonapi_extras\Normalizer\ConfigEntityDenormalizer + decorates: serializer.normalizer.config_entity.jsonapi + arguments: ['@serializer.normalizer.config_entity.jsonapi_extras.inner'] tags: - { name: normalizer, priority: 1022, format: api_json } @@ -42,10 +48,10 @@ services: jsonapi_extras.entity.to_jsonapi: class: Drupal\jsonapi_extras\EntityToJsonApi - arguments: ['@jsonapi.serializer_do_not_use_removal_imminent', '@jsonapi.resource_type.repository', '@current_user', '@request_stack', '%jsonapi.base_path%'] + arguments: ['@jsonapi.serializer', '@jsonapi.resource_type.repository', '@current_user', '@jsonapi.include_resolver'] - jsonapi.serializer_do_not_use_removal_imminent.jsonapi_extras: + jsonapi.serializer.jsonapi_extras: class: \Drupal\jsonapi_extras\SerializerDecorator public: false - decorates: jsonapi.serializer_do_not_use_removal_imminent - arguments: ['@jsonapi.serializer_do_not_use_removal_imminent.jsonapi_extras.inner'] + decorates: jsonapi.serializer + arguments: ['@jsonapi.serializer.jsonapi_extras.inner'] diff --git a/modules/jsonapi_defaults/src/Controller/EntityResource.php b/modules/jsonapi_defaults/src/Controller/EntityResource.php index 31a3da2..a60778d 100644 --- a/modules/jsonapi_defaults/src/Controller/EntityResource.php +++ b/modules/jsonapi_defaults/src/Controller/EntityResource.php @@ -15,6 +15,56 @@ use Symfony\Component\HttpFoundation\Request; */ class EntityResource extends JsonApiEntityResourse { + /** + * {@inheritdoc} + */ + protected function getJsonApiParams(Request $request, ResourceType $resource_type) { + // If this is a related resource, then we need to swap to the new resource + // type. + $related_field = $request->attributes->get('_on_relationship') + ? NULL + : $request->attributes->get('related'); + try { + $resource_type = static::correctResourceTypeOnRelated($related_field, $resource_type); + } + catch (\LengthException $e) { + watchdog_exception('jsonapi_defaults', $e); + $resource_type = NULL; + } + + if (!$resource_type instanceof ConfigurableResourceType) { + return parent::getJsonApiParams($request, $resource_type); + } + $resource_config = $resource_type->getJsonapiResourceConfig(); + if (!$resource_config instanceof JsonapiResourceConfig) { + return parent::getJsonApiParams($request, $resource_type); + } + $default_filter_input = $resource_config->getThirdPartySetting( + 'jsonapi_defaults', + 'default_filter', + [] + ); + + $default_filter = []; + foreach ($default_filter_input as $key => $value) { + if (substr($key, 0, 6) === 'filter') { + $key = str_replace('filter:', '', $key); + // TODO: Replace this with use of the NestedArray utility. + $this->setFilterValue($default_filter, $key, $value); + } + } + $filters = array_merge( + $default_filter, + $request->query->get('filter', []) + ); + + if (!empty($filters)) { + $request->query->set('filter', $filters); + } + + return parent::getJsonApiParams($request, $resource_type); + } + /** * {@inheritdoc} */ @@ -93,4 +143,24 @@ class EntityResource extends JsonApiEntityResourse { return $relatable_resource_types[0]; } + /** + * Set filter into nested array. + * + * @param array $arr + * The default filter. + * @param string $path + * The filter path. + * @param mixed $value + * The filter value. + */ + private function setFilterValue(array &$arr, $path, $value) { + $keys = explode('#', $path); + + foreach ($keys as $key) { + $arr = &$arr[$key]; + } + + $arr = $value; + } + } diff --git a/modules/jsonapi_defaults/src/JsonApiDefaultsJsonApiParamEnhancer.php b/modules/jsonapi_defaults/src/JsonApiDefaultsJsonApiParamEnhancer.php deleted file mode 100644 index 175c385..0000000 --- a/modules/jsonapi_defaults/src/JsonApiDefaultsJsonApiParamEnhancer.php +++ /dev/null @@ -1,112 +0,0 @@ -<?php - -namespace Drupal\jsonapi_defaults; - -use Drupal\jsonapi\Routing\JsonApiParamEnhancer; -use Drupal\jsonapi\Routing\Routes; -use Drupal\jsonapi_defaults\Controller\EntityResource; -use Drupal\jsonapi_extras\Entity\JsonapiResourceConfig; -use Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType; -use Symfony\Cmf\Component\Routing\RouteObjectInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\HttpFoundation\Request; - -/** - * JsonApiDefaultsJsonApiParamEnhancer class. - * - * @internal - */ -class JsonApiDefaultsJsonApiParamEnhancer extends JsonApiParamEnhancer { - - /** - * Configuration manager. - * - * @var \Drupal\Core\Config\ConfigManagerInterface - */ - protected $configManager; - - /** - * {@inheritdoc} - */ - public function setContainer(ContainerInterface $container = NULL) { - parent::setContainer($container); - - $this->configManager = $container->get('config.manager'); - } - - /** - * {@inheritdoc} - */ - public function enhance(array $defaults, Request $request) { - if (!Routes::isJsonApiRequest($defaults)) { - return parent::enhance($defaults, $request); - } - $resource_type = Routes::getResourceTypeNameFromParameters($defaults); - // If this is a related resource, then we need to swap to the new resource - // type. - $route = $defaults[RouteObjectInterface::ROUTE_OBJECT]; - $related_field = $route->getDefault('_on_relationship') - ? NULL - : $route->getDefault('related'); - try { - $resource_type = EntityResource::correctResourceTypeOnRelated($related_field, $resource_type); - } - catch (\LengthException $e) { - watchdog_exception('jsonapi_defaults', $e); - $resource_type = NULL; - } - - if (!$resource_type instanceof ConfigurableResourceType) { - return parent::enhance($defaults, $request); - } - $resource_config = $resource_type->getJsonapiResourceConfig(); - if (!$resource_config instanceof JsonapiResourceConfig) { - return parent::enhance($defaults, $request); - } - $default_filter_input = $resource_config->getThirdPartySetting( - 'jsonapi_defaults', - 'default_filter', - [] - ); - - $default_filter = []; - foreach ($default_filter_input as $key => $value) { - if (substr($key, 0, 6) === 'filter') { - $key = str_replace('filter:', '', $key); - // TODO: Replace this with use of the NestedArray utility. - $this->setFilterValue($default_filter, $key, $value); - } - } - $filters = array_merge( - $default_filter, - $request->query->get('filter', []) - ); - - if (!empty($filters)) { - $request->query->set('filter', $filters); - } - - return parent::enhance($defaults, $request); - } - - /** - * Set filter into nested array. - * - * @param array $arr - * The default filter. - * @param string $path - * The filter path. - * @param mixed $value - * The filter value. - */ - private function setFilterValue(array &$arr, $path, $value) { - $keys = explode('#', $path); - - foreach ($keys as $key) { - $arr = &$arr[$key]; - } - - $arr = $value; - } - -} diff --git a/modules/jsonapi_defaults/src/JsonapiDefaultsServiceProvider.php b/modules/jsonapi_defaults/src/JsonapiDefaultsServiceProvider.php index 7ff2060..60da4d7 100644 --- a/modules/jsonapi_defaults/src/JsonapiDefaultsServiceProvider.php +++ b/modules/jsonapi_defaults/src/JsonapiDefaultsServiceProvider.php @@ -16,11 +16,6 @@ class JsonapiDefaultsServiceProvider extends ServiceProviderBase { public function alter(ContainerBuilder $container) { /** @var \Symfony\Component\DependencyInjection\Definition $definition */ - if ($container->hasDefinition('jsonapi.params.enhancer')) { - $definition = $container->getDefinition('jsonapi.params.enhancer'); - $definition->setClass('Drupal\jsonapi_defaults\JsonApiDefaultsJsonApiParamEnhancer'); - } - if ($container->hasDefinition('jsonapi.entity_resource')) { $definition = $container->getDefinition('jsonapi.entity_resource'); $definition->setClass('Drupal\jsonapi_defaults\Controller\EntityResource'); diff --git a/src/EntityToJsonApi.php b/src/EntityToJsonApi.php index 236e804..bcea945 100644 --- a/src/EntityToJsonApi.php +++ b/src/EntityToJsonApi.php @@ -4,14 +4,13 @@ namespace Drupal\jsonapi_extras; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Session\AccountInterface; -use Drupal\jsonapi\JsonApiResource\EntityCollection; +use Drupal\jsonapi\IncludeResolver; use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel; use Drupal\jsonapi\JsonApiResource\LinkCollection; +use Drupal\jsonapi\JsonApiResource\NullEntityCollection; +use Drupal\jsonapi\JsonApiResource\ResourceObject; use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface; -use Drupal\jsonapi\Routing\Routes; use Drupal\jsonapi\Serializer\Serializer; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Serializer\SerializerInterface; /** @@ -43,18 +42,11 @@ class EntityToJsonApi { protected $resourceTypeRepository; /** - * The master request. + * The JSON:API include resolver. * - * @var \Symfony\Component\HttpFoundation\Request + * @var \Drupal\jsonapi\IncludeResolver */ - protected $masterRequest; - - /** - * The JSON:API base path. - * - * @var string - */ - protected $jsonApiBasePath; + protected $includeResolver; /** * EntityToJsonApi constructor. @@ -65,22 +57,15 @@ class EntityToJsonApi { * The resource type repository. * @param \Drupal\Core\Session\AccountInterface $current_user * The currently logged in user. - * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack - * The request stack. - * @param string $jsonapi_base_path - * The JSON:API base path. + * @param \Drupal\jsonapi\IncludeResolver $include_resolver + * The JSON:API include resolver. */ - public function __construct(SerializerInterface $serializer, ResourceTypeRepositoryInterface $resource_type_repository, AccountInterface $current_user, RequestStack $request_stack, $jsonapi_base_path) { + public function __construct(SerializerInterface $serializer, ResourceTypeRepositoryInterface $resource_type_repository, AccountInterface $current_user, IncludeResolver $include_resolver) { assert($serializer instanceof Serializer || $serializer instanceof SerializerDecorator); $this->serializer = $serializer; $this->resourceTypeRepository = $resource_type_repository; $this->currentUser = $current_user; - $this->masterRequest = $request_stack->getMasterRequest(); - assert(is_string($jsonapi_base_path)); - assert($jsonapi_base_path[0] === '/'); - assert(isset($jsonapi_base_path[1])); - assert(substr($jsonapi_base_path, -1) !== '/'); - $this->jsonApiBasePath = $jsonapi_base_path; + $this->includeResolver = $include_resolver; } /** @@ -95,12 +80,7 @@ class EntityToJsonApi { * The raw JSON string of the requested resource. */ public function serialize(EntityInterface $entity, array $includes = []) { - $context = $this->calculateContext($entity, $includes); - $document = $this->prepareDocument($entity, $includes, $context); - return $this->serializer->serialize($document, - 'api_json', - $context - ); + return $this->serializer->encode($this->normalize($entity, $includes), 'api_json'); } /** @@ -115,8 +95,10 @@ class EntityToJsonApi { * The JSON structure of the requested resource. */ public function normalize(EntityInterface $entity, array $includes = []) { - $context = $this->calculateContext($entity, $includes); - $document = $this->prepareDocument($entity, $includes, $context); + $resource_type = $this->resourceTypeRepository->get($entity->getEntityTypeId(), $entity->bundle()); + $resource_object = new ResourceObject($resource_type, $entity); + $context = $this->calculateContext($resource_object); + $document = $this->prepareDocument($resource_object, $includes); return $this->serializer->normalize($document, 'api_json', $context @@ -126,82 +108,35 @@ class EntityToJsonApi { /** * Calculate the arguments for the serialize/normalize operation. * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity to generate the JSON from. - * @param string[] $includes - * The list of includes. + * @param \Drupal\jsonapi\JsonApiResource\ResourceObject $resource_object + * The resource object to generate the JSON from. * * @return array * The list of arguments for serialize/normalize operation. */ - protected function calculateContext( - EntityInterface $entity, - array $includes = [] - ) { - $entity_type_id = $entity->getEntityTypeId(); - $resource_type = $this->resourceTypeRepository->get( - $entity_type_id, - $entity->bundle() - ); - // The overridden resource type implementation of "jsonapi_extras" may - // return a value containing a leading slash. Since this was initial - // behavior we won't going to break the things and ready to tackle both - // cases: with or without a leading slash. - $resource_path = ltrim($resource_type->getPath(), '/'); - $path = sprintf( - '%s/%s/%s', - rtrim($this->jsonApiBasePath, '/'), - rtrim($resource_path, '/'), - $entity->uuid() - ); - $request = Request::create($this->masterRequest->getUriForPath($path)); - - // We don't have to filter the "$include" since this will be done later. - // @see JsonApiDocumentTopLevelNormalizer::expandContext() - $request->query->set('include', implode(',', $includes)); - $request->attributes->set($entity_type_id, $entity); - $request->attributes->set(Routes::RESOURCE_TYPE_KEY, $resource_type); - $request->attributes->set(Routes::JSON_API_ROUTE_FLAG_KEY, TRUE); - + protected function calculateContext(ResourceObject $resource_object) { return [ 'account' => $this->currentUser, - 'resource_type' => $resource_type, - 'request' => $request, + 'resource_object' => $resource_object, ]; } /** * Prepare the JsonApi Document for serialization or normalization. * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity to prepare the document from. + * @param \Drupal\jsonapi\JsonApiResource\ResourceObject $resource_object + * The resource object to generate the JSON from. * @param array $includes * An array of included related entities to add to the document. - * @param array $context - * The prepared context. * * @return \Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel * The top level document. */ - private function prepareDocument(EntityInterface $entity, array $includes, array $context) { - /** @var \Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType $resource_type */ - $resource_type = $context['resource_type']; - $referenced_entities = []; - foreach ($includes as $public_name) { - // Ensure we are getting the proper field names of includes regardless of - // whether the public or internal name was passed in. - $internal_name = $resource_type->getInternalName($public_name); - $referenced_entities = array_merge( - $referenced_entities, - $entity->get($internal_name)->referencedEntities() - ); - } - $entity_collection = new EntityCollection($referenced_entities); - return new JsonApiDocumentTopLevel( - $entity, - $entity_collection, - new LinkCollection([]) - ); + private function prepareDocument(ResourceObject $resource_object, array $includes) { + $includes = !empty($includes) + ? $this->includeResolver->resolve($resource_object, implode(',', $includes)) + : new NullEntityCollection(); + return new JsonApiDocumentTopLevel($resource_object, $includes, new LinkCollection([])); } } diff --git a/src/JsonapiExtrasServiceProvider.php b/src/JsonapiExtrasServiceProvider.php index e7b35d3..6535285 100644 --- a/src/JsonapiExtrasServiceProvider.php +++ b/src/JsonapiExtrasServiceProvider.php @@ -34,19 +34,23 @@ class JsonapiExtrasServiceProvider extends ServiceProviderBase { if ($container->has('serializer.normalizer.field_item.jsonapi')) { $container->getDefinition('serializer.normalizer.field_item.jsonapi')->setPrivate(TRUE)->clearTags(); } - if ($container->has('serializer.normalizer.entity_reference_item.jsonapi')) { - $container->getDefinition('serializer.normalizer.entity_reference_item.jsonapi')->setPrivate(TRUE)->clearTags(); + if ($container->has('serializer.normalizer.resource_identifier.jsonapi')) { + $container->getDefinition('serializer.normalizer.resource_identifier.jsonapi')->setPrivate(TRUE)->clearTags(); } - if ($container->has('serializer.normalizer.entity.jsonapi')) { - $container->getDefinition('serializer.normalizer.entity.jsonapi')->setPrivate(TRUE)->clearTags(); + if ($container->has('serializer.normalizer.resource_object.jsonapi')) { + $container->getDefinition('serializer.normalizer.resource_object.jsonapi')->clearTags()->addMethodCall('setSerializer', [new Reference('jsonapi.serializer')]); + } + if ($container->has('serializer.normalizer.content_entity.jsonapi')) { + $definition = $container->getDefinition('serializer.normalizer.content_entity.jsonapi'); + $definition->clearTags()->addMethodCall('setSerializer', [new Reference('jsonapi.serializer')]); } if ($container->has('serializer.normalizer.config_entity.jsonapi')) { - $container->getDefinition('serializer.normalizer.config_entity.jsonapi')->setPrivate(TRUE)->clearTags(); + $container->getDefinition('serializer.normalizer.config_entity.jsonapi')->clearTags()->addMethodCall('setSerializer', [new Reference('jsonapi.serializer')]); } // Break a circular dependency. // @see \Drupal\jsonapi_extras\SerializerDecorator::lazilyInitialize() - $definition = $container->getDefinition('jsonapi.serializer_do_not_use_removal_imminent'); + $definition = $container->getDefinition('jsonapi.serializer'); $definition->removeMethodCall('setFallbackNormalizer'); $settings = BootstrapConfigStorageFactory::get() diff --git a/src/Normalizer/ConfigEntityDenormalizer.php b/src/Normalizer/ConfigEntityDenormalizer.php new file mode 100644 index 0000000..469bf2f --- /dev/null +++ b/src/Normalizer/ConfigEntityDenormalizer.php @@ -0,0 +1,28 @@ +<?php + +namespace Drupal\jsonapi_extras\Normalizer; + +use Drupal\jsonapi\ResourceType\ResourceType; + +/** + * Override ConfigEntityNormalizer to prepare input. + */ +class ConfigEntityDenormalizer extends ContentEntityDenormalizer { + + /** + * {@inheritdoc} + */ + protected function prepareInput(array $data, ResourceType $resource_type) { + foreach ($data as $public_field_name => &$field_value) { + /** @var \Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerInterface $enhancer */ + $enhancer = $resource_type->getFieldEnhancer($public_field_name); + if (!$enhancer) { + continue; + } + $field_value = $enhancer->transform($field_value); + } + + return $data; + } + +} diff --git a/src/Normalizer/ConfigEntityNormalizer.php b/src/Normalizer/ConfigEntityNormalizer.php deleted file mode 100644 index 003dd06..0000000 --- a/src/Normalizer/ConfigEntityNormalizer.php +++ /dev/null @@ -1,48 +0,0 @@ -<?php - -namespace Drupal\jsonapi_extras\Normalizer; - -use Drupal\jsonapi\Normalizer\ConfigEntityNormalizer as JsonapiConfigEntityNormalizer; -use Drupal\jsonapi\ResourceType\ResourceType; - -/** - * Override ConfigEntityNormalizer to prepare input. - */ -class ConfigEntityNormalizer extends JsonapiConfigEntityNormalizer { - - use EntityNormalizerTrait; - - /** - * {@inheritdoc} - */ - protected function getFields($entity, $bundle, ResourceType $resource_type) { - $enabled_public_fields = parent::getFields($entity, $bundle, $resource_type); - // Then detect if there is any enhancer to be applied here. - foreach ($enabled_public_fields as $field_name => &$field_value) { - $enhancer = $resource_type->getFieldEnhancer($field_name); - if (!$enhancer) { - continue; - } - $field_value = $enhancer->undoTransform($field_value); - } - - return $enabled_public_fields; - } - - /** - * {@inheritdoc} - */ - protected function prepareInput(array $data, ResourceType $resource_type, $format, array $context) { - foreach ($data as $public_field_name => &$field_value) { - /** @var \Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerInterface $enhancer */ - $enhancer = $resource_type->getFieldEnhancer($public_field_name); - if (!$enhancer) { - continue; - } - $field_value = $enhancer->transform($field_value); - } - - return parent::prepareInput($data, $resource_type, $format, $context); - } - -} diff --git a/src/Normalizer/EntityNormalizerTrait.php b/src/Normalizer/ContentEntityDenormalizer.php similarity index 67% rename from src/Normalizer/EntityNormalizerTrait.php rename to src/Normalizer/ContentEntityDenormalizer.php index ca16a30..b58812c 100644 --- a/src/Normalizer/EntityNormalizerTrait.php +++ b/src/Normalizer/ContentEntityDenormalizer.php @@ -3,11 +3,41 @@ namespace Drupal\jsonapi_extras\Normalizer; use Drupal\jsonapi\ResourceType\ResourceType; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; /** - * Common code for entity normalizers. + * Override ContentEntityNormalizer to prepare input. */ -trait EntityNormalizerTrait { +class ContentEntityDenormalizer implements DenormalizerInterface { + + /** + * @var \Symfony\Component\Serializer\Normalizer\DenormalizerInterface + */ + protected $inner; + + /** + * ContentEntityDenormalizer constructor. + * + * @param \Symfony\Component\Serializer\Normalizer\DenormalizerInterface $inner + * The JSON:API content entity denormalizer. + */ + public function __construct(DenormalizerInterface $inner) { + $this->inner = $inner; + } + + /** + * {@inheritdoc} + */ + public function denormalize($data, $class, $format = NULL, array $context = []) { + return $this->inner->denormalize($this->prepareInput($data, $context['resource_type']), $class, $format, $context); + } + + /** + * {@inheritdoc} + */ + public function supportsDenormalization($data, $type, $format = NULL) { + return $this->inner->supportsDenormalization($data, $type, $format); + } /** * Prepares the input data to create the entity. @@ -16,15 +46,11 @@ trait EntityNormalizerTrait { * The input data to modify. * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type * Contains the info about the resource type. - * @param string $format - * Format from which the given data was extracted. - * @param array $context - * Options available to the denormalizer. * * @return array * The modified input data. */ - protected function prepareInput(array $data, ResourceType $resource_type, $format, array $context) { + protected function prepareInput(array $data, ResourceType $resource_type) { /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_storage_definitions */ $field_storage_definitions = \Drupal::service('entity_field.manager') ->getFieldStorageDefinitions( @@ -37,7 +63,7 @@ trait EntityNormalizerTrait { // Skip any disabled field. $internal_name = $resource_type->getInternalName($public_field_name); $entity_type_id = $resource_type->getEntityTypeId(); - $entity_type_definition = $this->entityTypeManager->getDefinition($entity_type_id); + $entity_type_definition = \Drupal::entityTypeManager()->getDefinition($entity_type_id); $uuid_key = $entity_type_definition->getKey('uuid'); if (!$resource_type->isFieldEnabled($internal_name) && $uuid_key !== $internal_name) { continue; @@ -69,28 +95,7 @@ trait EntityNormalizerTrait { $data_internal[$public_field_name] = $field_value; } - return parent::prepareInput($data_internal, $resource_type, $format, $context); - } - - /** - * Get the configuration entity based on the entity type and bundle. - * - * @param string $entity_type_id - * The entity type ID. - * @param string $bundle_id - * The bundle ID. - * - * @return \Drupal\Core\Entity\EntityInterface|null - * The resource config entity or NULL. - */ - protected function getResourceConfig($entity_type_id, $bundle_id) { - $id = sprintf('%s--%s', $entity_type_id, $bundle_id); - // TODO: Inject this service. - $resource_config = \Drupal::entityTypeManager() - ->getStorage('jsonapi_resource_config') - ->load($id); - - return $resource_config; + return $data_internal; } } diff --git a/src/Normalizer/ContentEntityNormalizer.php b/src/Normalizer/ContentEntityNormalizer.php deleted file mode 100644 index 302866a..0000000 --- a/src/Normalizer/ContentEntityNormalizer.php +++ /dev/null @@ -1,28 +0,0 @@ -<?php - -namespace Drupal\jsonapi_extras\Normalizer; - -use Drupal\jsonapi\Normalizer\ContentEntityNormalizer as JsonapiContentEntityNormalizer; -use Symfony\Component\Serializer\SerializerInterface; - -/** - * Override ContentEntityNormalizer to prepare input. - */ -class ContentEntityNormalizer extends JsonapiContentEntityNormalizer { - - use EntityNormalizerTrait; - - /** - * {@inheritdoc} - */ - public function setSerializer(SerializerInterface $serializer) { - // The first invocation is made by the container builder, it respects the - // service definition. We respect this. - // The second invocation is made by the Serializer service constructor, it - // does not respect the service definition. We ignore this call. - if (!isset($this->serializer)) { - parent::setSerializer($serializer); - } - } - -} diff --git a/src/Normalizer/FieldItemNormalizer.php b/src/Normalizer/FieldItemNormalizer.php index e993818..91982f7 100644 --- a/src/Normalizer/FieldItemNormalizer.php +++ b/src/Normalizer/FieldItemNormalizer.php @@ -68,7 +68,7 @@ class FieldItemNormalizer extends NormalizerBase implements DenormalizerInterfac $normalized_output = $this->subject->normalize($object, $format, $context); // Then detect if there is any enhancer to be applied here. /** @var \Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType $resource_type */ - $resource_type = $context['resource_type']; + $resource_type = $context['resource_object']->getResourceType(); $enhancer = $resource_type->getFieldEnhancer($object->getParent()->getName()); if (!$enhancer) { return $normalized_output; diff --git a/src/Normalizer/RelationshipItemNormalizer.php b/src/Normalizer/RelationshipItemNormalizer.php index 2a7d3e7..dce1417 100644 --- a/src/Normalizer/RelationshipItemNormalizer.php +++ b/src/Normalizer/RelationshipItemNormalizer.php @@ -2,15 +2,17 @@ namespace Drupal\jsonapi_extras\Normalizer; +use Drupal\jsonapi\JsonApiResource\ResourceIdentifier; use Drupal\jsonapi\Normalizer\NormalizerBase; use Drupal\jsonapi\Normalizer\RelationshipItem; -use Drupal\jsonapi\Normalizer\RelationshipItemNormalizer as RelationshipItemNormalizerJsonapi; +use Drupal\jsonapi\Normalizer\ResourceIdentifierNormalizer as DecoratedNormalizer; use Drupal\jsonapi\Normalizer\Value\CacheableNormalization; use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface; use Drupal\serialization\Normalizer\CacheableNormalizerInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\SerializerAwareInterface; use Symfony\Component\Serializer\SerializerAwareTrait; +use Symfony\Component\Serializer\SerializerInterface; /** * Converts the Drupal entity reference item object to a JSON:API structure. @@ -26,7 +28,7 @@ class RelationshipItemNormalizer extends NormalizerBase implements SerializerAwa * * @var string */ - protected $supportedInterfaceOrClass = RelationshipItem::class; + protected $supportedInterfaceOrClass = ResourceIdentifier::class; /** * The JSON:API base normalizer. @@ -43,14 +45,14 @@ class RelationshipItemNormalizer extends NormalizerBase implements SerializerAwa protected $resourceTypeRepository; /** - * Instantiates a RelationshipItemNormalizer object. + * Instantiates a ResourceIdentifierNormalizer object. * - * @param \Drupal\jsonapi\Normalizer\RelationshipItemNormalizer $subject + * @param \Drupal\jsonapi\Normalizer\ResourceIdentifierNormalizer $subject * The decorated normalizer. * @param \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository * The repository. */ - public function __construct(RelationshipItemNormalizerJsonapi $subject, ResourceTypeRepositoryInterface $resource_type_repository) { + public function __construct(DecoratedNormalizer $subject, ResourceTypeRepositoryInterface $resource_type_repository) { $this->subject = $subject; $this->resourceTypeRepository = $resource_type_repository; } @@ -58,11 +60,20 @@ class RelationshipItemNormalizer extends NormalizerBase implements SerializerAwa /** * {@inheritdoc} */ - public function normalize($relationship_item, $format = NULL, array $context = []) { - $normalized_output = $this->subject->normalize($relationship_item, $format, $context); + public function setSerializer(SerializerInterface $serializer) { + parent::setSerializer($serializer); + $this->subject->setSerializer($serializer); + } + + /** + * {@inheritdoc} + */ + public function normalize($field, $format = NULL, array $context = []) { + assert($field instanceof ResourceIdentifier); + $normalized_output = $this->subject->normalize($field, $format, $context); /** @var \Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType $resource_type */ - $resource_type = $context['resource_type']; - $enhancer = $resource_type->getFieldEnhancer($relationship_item->getParent()->getPropertyName()); + $resource_type = $context['resource_object']->getResourceType(); + $enhancer = $resource_type->getFieldEnhancer($context['field_name']); if (!$enhancer) { return $normalized_output; } diff --git a/src/Normalizer/ResourceObjectNormalizer.php b/src/Normalizer/ResourceObjectNormalizer.php new file mode 100644 index 0000000..edd463b --- /dev/null +++ b/src/Normalizer/ResourceObjectNormalizer.php @@ -0,0 +1,71 @@ +<?php + +namespace Drupal\jsonapi_extras\Normalizer; + +use Drupal\Core\Config\Entity\ConfigEntityInterface; +use Drupal\jsonapi\JsonApiResource\ResourceObject; +use Drupal\jsonapi\Normalizer\Value\CacheableNormalization; +use Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +/** + * Decorates the JSON:API ResourceObjectNormalizer. + * + * @internal + */ +class ResourceObjectNormalizer implements NormalizerInterface { + + /** + * @var \Symfony\Component\Serializer\Normalizer\NormalizerInterface + */ + protected $inner; + + public function __construct(NormalizerInterface $inner) { + $this->inner = $inner; + } + + public function supportsNormalization($data, $format = NULL) { + return $this->inner->supportsNormalization($data, $format); + } + + /** + * {@inheritdoc} + */ + public function normalize($object, $format = NULL, array $context = []) { + assert($object instanceof ResourceObject); + $resource_type = $object->getResourceType(); + $cacheable_normalization = $this->inner->normalize($object, $format, $context); + assert($cacheable_normalization instanceof CacheableNormalization); + if (is_subclass_of($resource_type->getDeserializationTargetClass(), ConfigEntityInterface::class)) { + return new CacheableNormalization( + $cacheable_normalization, + static::enhanceConfigFields($cacheable_normalization->getNormalization(), $resource_type) + ); + } + return $cacheable_normalization; + } + + /** + * Applies field enhancers to a config entity normalization. + * + * @param array $normalization + * The normalization to be enhanced. + * @param \Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType $resource_type + * The resource type of the normalized resource object. + * + * @return array + */ + protected static function enhanceConfigFields(array $normalization, ConfigurableResourceType $resource_type) { + if (!empty($normalization['attributes'])) { + foreach ($normalization['attributes'] as $field_name => $field_value) { + $enhancer = $resource_type->getFieldEnhancer($field_name); + if (!$enhancer) { + continue; + } + $normalization[$field_name] = $enhancer->undoTransform($field_value); + } + } + return $normalization; + } + +} \ No newline at end of file diff --git a/tests/src/Kernel/Controller/EntityResourceTest.php b/tests/src/Kernel/Controller/EntityResourceTest.php index c555155..1e5e077 100644 --- a/tests/src/Kernel/Controller/EntityResourceTest.php +++ b/tests/src/Kernel/Controller/EntityResourceTest.php @@ -5,6 +5,7 @@ namespace Drupal\Tests\jsonapi_extras\Kernel\Controller; use Drupal\Component\Serialization\Json; use Drupal\Core\Config\ConfigException; use Drupal\jsonapi\Access\EntityAccessChecker; +use Drupal\jsonapi\JsonApiResource\ResourceObject; use Drupal\jsonapi\ResourceType\ResourceType; use Drupal\jsonapi\Controller\EntityResource; use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel as JsonApiDocumentTopLevel2; @@ -16,7 +17,7 @@ use Symfony\Component\HttpFoundation\Request; /** * @coversDefaultClass \Drupal\jsonapi\Controller\EntityResource - * @covers \Drupal\jsonapi_extras\Normalizer\ConfigEntityNormalizer + * @covers \Drupal\jsonapi_extras\Normalizer\ConfigEntityDenormalizer * @group jsonapi_extras * @group legacy * @@ -53,15 +54,21 @@ class EntityResourceTest extends KernelTestBase { * @covers ::createIndividual */ public function testCreateIndividualConfig() { - $node_type = NodeType::create([ - 'type' => 'test', - 'name' => 'Test Type', - 'description' => 'Lorem ipsum', - ]); Role::load(Role::ANONYMOUS_ID) ->grantPermission('administer content types') ->save(); - $resource_type = new ResourceType('node', 'article', NULL); + $resource_type = new ResourceType('node_type', 'node_type', NodeType::class); + $resource_type->setRelatableResourceTypes([]); + $payload = Json::encode([ + 'data' => [ + 'type' => 'node--test', + 'attributes' => [ + 'type' => 'test', + 'name' => 'Test Type', + 'description' => 'Lorem ipsum', + ], + ], + ]); $entity_resource = new EntityResource( $this->container->get('entity_type.manager'), $this->container->get('entity_field.manager'), @@ -75,13 +82,17 @@ class EntityResourceTest extends KernelTestBase { $this->container->get('router.no_access_checks'), $this->container->get('current_user'), $this->container->get('entity.repository') - ) + ), + $this->container->get('jsonapi.field_resolver'), + $this->container->get('jsonapi.serializer') ); - $response = $entity_resource->createIndividual($resource_type, $node_type, Request::create('/jsonapi/node/test')); + $response = $entity_resource->createIndividual($resource_type, Request::create('/jsonapi/node_type/node_type', 'POST', [], [], [], [], $payload)); // As a side effect, the node type will also be saved. - $this->assertNotEmpty($node_type->id()); + $node_type = NodeType::load('test'); $this->assertInstanceOf(JsonApiDocumentTopLevel2::class, $response->getResponseData()); - $this->assertEquals('test', $response->getResponseData()->getData()->id()); + $data = $response->getResponseData()->getData(); + $this->assertInstanceOf(ResourceObject::class, $data); + $this->assertEquals($node_type->uuid(), $data->getId()); $this->assertEquals(201, $response->getStatusCode()); } @@ -108,14 +119,17 @@ class EntityResourceTest extends KernelTestBase { ->save(); $payload = Json::encode([ 'data' => [ - 'type' => 'node_type', + 'type' => 'node_type--node_type', 'id' => $node_type->uuid(), 'attributes' => $values, ], ]); $request = Request::create('/jsonapi/node/node_type/' . $node_type->uuid(), 'PATCH', [], [], [], [], $payload); - $resource_type = new ResourceType('node', 'article', NULL); + $resource_type = new ResourceType('node_type', 'node_type', NodeType::class, FALSE, TRUE, TRUE, FALSE, [ + 'type' => 'drupal_internal__type', + ]); + $resource_type->setRelatableResourceTypes([]); $entity_resource = new EntityResource( $this->container->get('entity_type.manager'), $this->container->get('entity_field.manager'), @@ -129,14 +143,16 @@ class EntityResourceTest extends KernelTestBase { $this->container->get('router.no_access_checks'), $this->container->get('current_user'), $this->container->get('entity.repository') - ) + ), + $this->container->get('jsonapi.field_resolver'), + $this->container->get('jsonapi.serializer') ); - $response = $entity_resource->patchIndividual($resource_type, $node_type, $parsed_node_type, $request); + $response = $entity_resource->patchIndividual($resource_type, $node_type, $request); // As a side effect, the node will also be saved. $this->assertInstanceOf(JsonApiDocumentTopLevel2::class, $response->getResponseData()); $updated_node_type = $response->getResponseData()->getData(); - $this->assertInstanceOf(NodeType::class, $updated_node_type); + $this->assertInstanceOf(ResourceObject::class, $updated_node_type); // If the field is ignored then we should not see a difference. foreach ($values as $field_name => $value) { in_array($field_name, $ignored_fields) ? @@ -163,8 +179,8 @@ class EntityResourceTest extends KernelTestBase { * @covers ::patchIndividual * @dataProvider patchIndividualConfigFailedProvider */ - public function testPatchIndividualFailedConfig($values) { - $this->setExpectedException(ConfigException::class); + public function testPatchIndividualFailedConfig($values, $expected_message) { + $this->setExpectedException(ConfigException::class, $expected_message); $this->testPatchIndividualConfig($values); } @@ -176,8 +192,7 @@ class EntityResourceTest extends KernelTestBase { */ public function patchIndividualConfigFailedProvider() { return [ - [['uuid' => 'PATCHED']], - [['type' => 'article', 'status' => FALSE]], + [['type' => 'article', 'status' => FALSE], "The machine name of the 'Content type' bundle cannot be changed."], ]; } @@ -208,7 +223,9 @@ class EntityResourceTest extends KernelTestBase { $this->container->get('router.no_access_checks'), $this->container->get('current_user'), $this->container->get('entity.repository') - ) + ), + $this->container->get('jsonapi.field_resolver'), + $this->container->get('jsonapi.serializer') ); $response = $entity_resource->deleteIndividual($node_type, new Request()); // As a side effect, the node will also be deleted. -- GitLab