Unverified Commit 3434a888 authored by Wim Leers's avatar Wim Leers Committed by Mateu Aguiló Bosch

Issue #3004582 by Wim Leers, e0ipso, gabesullice: Make JSON API Extras 2.x...

Issue #3004582 by Wim Leers, e0ipso, gabesullice: Make JSON API Extras 2.x (next release: 2.10) compatible with JSON API 2.0-beta2
parent 8cbaa2ba
......@@ -6,18 +6,20 @@ services:
- '@entity_type.manager'
- '@plugin.manager.resource_field_enhancer'
tags:
- { name: jsonapi_normalizer_do_not_use_removal_imminent, priority: 25 }
- { name: normalizer, priority: 1025, format: api_json }
serializer.normalizer.entity.jsonapi_extras:
class: Drupal\jsonapi_extras\Normalizer\ContentEntityNormalizer
arguments: ['@jsonapi.link_manager', '@jsonapi.resource_type.repository', '@entity_type.manager']
parent: serializer.normalizer.entity.jsonapi
calls:
- [setSerializer, ['@jsonapi.serializer_do_not_use_removal_imminent']]
tags:
- { name: jsonapi_normalizer_do_not_use_removal_imminent, priority: 22 }
- { name: normalizer, priority: 1022, format: api_json }
serializer.normalizer.config_entity.jsonapi_extras:
class: Drupal\jsonapi_extras\Normalizer\ConfigEntityNormalizer
arguments: ['@jsonapi.link_manager', '@jsonapi.resource_type.repository', '@entity_type.manager']
parent: serializer.normalizer.config_entity.jsonapi
tags:
- { name: jsonapi_normalizer_do_not_use_removal_imminent, priority: 22 }
- { name: normalizer, priority: 1022, format: api_json }
plugin.manager.resource_field_enhancer:
class: Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerManager
......@@ -33,3 +35,9 @@ 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%']
jsonapi.serializer_do_not_use_removal_imminent.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']
......@@ -123,4 +123,31 @@ class JsonapiResourceConfig extends ConfigEntityBase {
}
}
/**
* Returns a field mapping as expected by JSON API 2.x' ResourceType class.
*
* @see \Drupal\jsonapi\ResourceType\ResourceType::__construct()
*/
public function getFieldMapping() {
$resource_fields = $this->get('resourceFields');
$mapping = [];
foreach ($resource_fields as $resource_field) {
$field_name = $resource_field['fieldName'];
if ($resource_field['disabled'] === TRUE) {
$mapping[$field_name] = FALSE;
continue;
}
if (($alias = $resource_field['publicName']) && $alias !== $field_name) {
$mapping[$field_name] = $alias;
continue;
}
$mapping[$field_name] = TRUE;
}
return $mapping;
}
}
......@@ -4,12 +4,14 @@ namespace Drupal\jsonapi_extras;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\jsonapi\Resource\JsonApiDocumentTopLevel;
use Drupal\jsonapi\JsonApiResource\EntityCollection;
use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface;
use Drupal\jsonapi\Routing\Routes;
use Drupal\jsonapi\Serializer\Serializer;
use Drupal\jsonapi_extras\ResourceType\ConfigurableResourceTypeRepository;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Serializer\SerializerInterface;
/**
* Simplifies the process of generating a JSON API version of an entity.
......@@ -56,7 +58,7 @@ class EntityToJsonApi {
/**
* EntityToJsonApi constructor.
*
* @param \Drupal\jsonapi\Serializer\Serializer $serializer
* @param \Drupal\jsonapi\Serializer\Serializer|\Drupal\jsonapi_extras\SerializerDecorator $serializer
* The serializer.
* @param \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository
* The resource type repository.
......@@ -67,7 +69,8 @@ class EntityToJsonApi {
* @param string $jsonapi_base_path
* The JSON API base path.
*/
public function __construct(Serializer $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, RequestStack $request_stack, $jsonapi_base_path) {
assert($serializer instanceof Serializer || $serializer instanceof SerializerDecorator);
$this->serializer = $serializer;
$this->resourceTypeRepository = $resource_type_repository;
$this->currentUser = $current_user;
......@@ -77,6 +80,9 @@ class EntityToJsonApi {
assert(isset($jsonapi_base_path[1]));
assert(substr($jsonapi_base_path, -1) !== '/');
$this->jsonApiBasePath = $jsonapi_base_path;
$this->classToUse = ConfigurableResourceTypeRepository::isJsonApi2x()
? '\Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel'
: '\Drupal\jsonapi\Resource\JsonApiDocumentTopLevel';
}
/**
......@@ -91,7 +97,14 @@ class EntityToJsonApi {
* The raw JSON string of the requested resource.
*/
public function serialize(EntityInterface $entity, array $includes = []) {
return $this->serializer->serialize(new JsonApiDocumentTopLevel($entity),
$referenced_entities = [];
foreach ($includes as $field_name) {
$referenced_entities = array_merge($referenced_entities, $entity->get($field_name)->referencedEntities());
}
$document = ConfigurableResourceTypeRepository::isJsonApi2x()
? new $this->classToUse($entity, new EntityCollection($referenced_entities), [])
: new $this->classToUse($entity);
return $this->serializer->serialize($document,
'api_json',
$this->calculateContext($entity, $includes)
);
......@@ -109,7 +122,14 @@ class EntityToJsonApi {
* The JSON structure of the requested resource.
*/
public function normalize(EntityInterface $entity, array $includes = []) {
return $this->serializer->normalize(new JsonApiDocumentTopLevel($entity),
$referenced_entities = [];
foreach ($includes as $field_name) {
$referenced_entities = array_merge($referenced_entities, $entity->get($field_name)->referencedEntities());
}
$document = ConfigurableResourceTypeRepository::isJsonApi2x()
? new $this->classToUse($entity, new EntityCollection($referenced_entities), [])
: new $this->classToUse($entity);
return $this->serializer->normalize($document,
'api_json',
$this->calculateContext($entity, $includes)
)->rasterizeValue();
......
......@@ -23,11 +23,29 @@ class JsonapiExtrasServiceProvider extends ServiceProviderBase {
$definition->setClass(ConfigurableResourceTypeRepository::class);
// The configurable service expects the entity repository and the enhancer
// plugin manager.
$definition->addArgument(new Reference('entity.repository'));
$definition->addArgument(new Reference('plugin.manager.resource_field_enhancer'));
$definition->addArgument(new Reference('config.factory'));
$definition->addMethodCall('setEntityRepository', [new Reference('entity.repository')]);
$definition->addMethodCall('setEnhancerManager', [new Reference('plugin.manager.resource_field_enhancer')]);
$definition->addMethodCall('setConfigFactory', [new Reference('config.factory')]);
}
// Make all three of the normalizers that JSON API Extras overrides private
// untagged services, to ensure that JSON API Extras' overrides continue to
// work in JSON API 2.x, using core's @serializer service.
if ($container->has('serializer.normalizer.field_item.jsonapi')) {
$container->getDefinition('serializer.normalizer.field_item.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.config_entity.jsonapi')) {
$container->getDefinition('serializer.normalizer.config_entity.jsonapi')->setPrivate(TRUE)->clearTags();
}
// Break a circular dependency.
// @see \Drupal\jsonapi_extras\SerializerDecorator::lazilyInitialize()
$definition = $container->getDefinition('jsonapi.serializer_do_not_use_removal_imminent');
$definition->removeMethodCall('setFallbackNormalizer');
$settings = BootstrapConfigStorageFactory::get()
->read('jsonapi_extras.settings');
......
......@@ -32,7 +32,7 @@ class ConfigEntityNormalizer extends JsonapiConfigEntityNormalizer {
/**
* {@inheritdoc}
*/
protected function prepareInput(array $data, ResourceType $resource_type) {
protected function prepareInput(array $data, ResourceType $resource_type, $format = NULL, array $context = []) {
foreach ($data as $public_field_name => &$field_value) {
/** @var \Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerInterface $enhancer */
$enhancer = $resource_type->getFieldEnhancer($public_field_name);
......@@ -42,7 +42,7 @@ class ConfigEntityNormalizer extends JsonapiConfigEntityNormalizer {
$field_value = $enhancer->transform($field_value);
}
return parent::prepareInput($data, $resource_type);
return parent::prepareInput($data, $resource_type, $format, $context);
}
}
......@@ -3,6 +3,7 @@
namespace Drupal\jsonapi_extras\Normalizer;
use Drupal\jsonapi\Normalizer\ContentEntityNormalizer as JsonapiContentEntityNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
/**
* Override ContentEntityNormalizer to prepare input.
......@@ -11,4 +12,17 @@ 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);
}
}
}
......@@ -16,11 +16,19 @@ trait EntityNormalizerTrait {
* The input data to modify.
* @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
* Contains the info about the resource type.
* @param string $format
* (optional) Format from which the given data was extracted. Only required
* for JSON API 2.x.
* @param array $context
* (optional) Options available to the denormalizer. Only required for JSON
* API 2.x.
*
* @return array
* The modified input data.
*
* @todo Make the last 2 args non-optional when JSON API 2.x is required.
*/
protected function prepareInput(array $data, ResourceType $resource_type) {
protected function prepareInput(array $data, ResourceType $resource_type, $format = NULL, array $context = []) {
/** @var \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_storage_definitions */
$field_storage_definitions = \Drupal::service('entity_field.manager')
->getFieldStorageDefinitions(
......
......@@ -2,6 +2,7 @@
namespace Drupal\jsonapi_extras\Plugin\jsonapi\FieldEnhancer;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\jsonapi_extras\Plugin\DateTimeEnhancerBase;
use Shaper\Util\Context;
......@@ -21,7 +22,10 @@ class DateTimeEnhancer extends DateTimeEnhancerBase {
*/
protected function doUndoTransform($data, Context $context) {
$date = new \DateTime();
$date->setTimestamp($data);
$date->setTimestamp(is_int($data)
? $data
: (new DrupalDateTime($data))->getTimestamp()
);
$configuration = $this->getConfiguration();
return $date->format($configuration['dateTimeFormat']);
......
......@@ -3,6 +3,7 @@
namespace Drupal\jsonapi_extras\ResourceType;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\jsonapi\ResourceType\ResourceType;
use Drupal\jsonapi_extras\Entity\JsonapiResourceConfig;
use Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerManager;
......@@ -13,6 +14,8 @@ use Drupal\Core\Config\ConfigFactoryInterface;
*/
class ConfigurableResourceType extends ResourceType {
use DependencySerializationTrait;
/**
* The JsonapiResourceConfig entity.
*
......@@ -36,6 +39,8 @@ class ConfigurableResourceType extends ResourceType {
/**
* {@inheritdoc}
*
* @todo Remove this when JSON API Extras drops support for JSON API 1.x.
*/
public function getPublicName($field_name) {
return $this->translateFieldName($field_name, 'fieldName', 'publicName');
......@@ -43,6 +48,8 @@ class ConfigurableResourceType extends ResourceType {
/**
* {@inheritdoc}
*
* @todo Remove this when JSON API Extras drops support for JSON API 1.x.
*/
public function getInternalName($field_name) {
return $this->translateFieldName($field_name, 'publicName', 'fieldName');
......@@ -74,6 +81,8 @@ class ConfigurableResourceType extends ResourceType {
/**
* {@inheritdoc}
*
* @todo Remove this when JSON API Extras drops support for JSON API 1.x.
*/
public function isFieldEnabled($field_name) {
$resource_field = $this->getResourceFieldConfiguration($field_name);
......@@ -155,6 +164,8 @@ class ConfigurableResourceType extends ResourceType {
*
* @param bool $is_internal
* Indicates if the resource is not public.
*
* @todo Remove this when JSON API Extras drops support for JSON API 1.x.
*/
public function setInternal($is_internal) {
$this->internal = $is_internal;
......@@ -211,6 +222,8 @@ class ConfigurableResourceType extends ResourceType {
*
* @return string
* The field name in the desired realm.
*
* @todo Remove this when JSON API Extras drops support for JSON API 1.x.
*/
private function translateFieldName($field_name, $from, $to) {
$resource_field = $this->getResourceFieldConfiguration($field_name, $from);
......
......@@ -2,14 +2,11 @@
namespace Drupal\jsonapi_extras\ResourceType;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
use Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerManager;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
/**
* Provides a repository of JSON API configurable resource types.
......@@ -18,6 +15,8 @@ class ConfigurableResourceTypeRepository extends ResourceTypeRepository {
/**
* {@inheritdoc}
*
* @todo Remove this when JSON API Extras drops support for JSON API 1.x.
*/
const RESOURCE_TYPE_CLASS = ConfigurableResourceType::class;
......@@ -35,13 +34,6 @@ class ConfigurableResourceTypeRepository extends ResourceTypeRepository {
*/
protected $enhancerManager;
/**
* The bundle manager.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $bundleManager;
/**
* The configuration factory.
*
......@@ -71,28 +63,61 @@ class ConfigurableResourceTypeRepository extends ResourceTypeRepository {
protected $resourceConfigs;
/**
* {@inheritdoc}
* Detects whether this site has JSON API 1.x or 2.x installed.
*
* One of the BC breaks in 2.x is the removal of JSON API 1.x's custom
* computed "url" field to File entities. Hence its presence or absence is
* also a very reliable detection mechanism.
*
* @return bool
* TRUE if JSON API 2.x is installed. FALSE otherwise.
*
* @todo Remove this when JSON API Extras drops support for JSON API 1.x.
*/
public static function isJsonApi2x() {
return !class_exists('\Drupal\jsonapi\Field\FileDownloadUrl');
}
/**
* Injects the entity repository.
*
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $bundle_manager, EntityFieldManagerInterface $entity_field_manager, EntityRepositoryInterface $entity_repository, ResourceFieldEnhancerManager $enhancer_manager, ConfigFactoryInterface $config_factory) {
parent::__construct($entity_type_manager, $bundle_manager, $entity_field_manager);
public function setEntityRepository(EntityRepositoryInterface $entity_repository) {
$this->entityRepository = $entity_repository;
}
/**
* Injects the resource enhancer manager.
*
* @param \Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerManager $enhancer_manager
* The resource enhancer manager.
*/
public function setEnhancerManager(ResourceFieldEnhancerManager $enhancer_manager) {
$this->enhancerManager = $enhancer_manager;
$this->configFactory = $config_factory;
$this->entityFieldManager = $entity_field_manager;
$this->bundleManager = $bundle_manager;
}
/**
* {@inheritdoc}
* Injects the configuration factory.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
*/
protected static function isMutableResourceType(EntityTypeInterface $entity_type) {
return TRUE;
public function setConfigFactory(ConfigFactoryInterface $config_factory) {
$this->configFactory = $config_factory;
}
/**
* {@inheritdoc}
*
* @todo Remove this when JSON API Extras drops support for JSON API 1.x.
*/
public function all() {
if (static::isJsonApi2x()) {
return parent::all();
}
if (!$this->all) {
$all = parent::all();
array_walk($all, [$this, 'injectAdditionalServicesToResourceType']);
......@@ -101,6 +126,43 @@ class ConfigurableResourceTypeRepository extends ResourceTypeRepository {
return $this->all;
}
/**
* {@inheritdoc}
*
* Mostly the same as the parent implementation, with three key differences:
* 1. Different resource type class.
* 2. Every resource type is assumed to be mutable.
* 2. Field mapping not based on logic, but on configuration.
*/
protected function createResourceType(EntityTypeInterface $entity_type, $bundle) {
$resource_config_id = sprintf(
'%s--%s',
$entity_type->id(),
$bundle
);
$resource_config = $this->getResourceConfig($resource_config_id);
// Create subclassed ResourceType object with the same parameters as the
// parent implementation.
$resource_type = new ConfigurableResourceType(
$entity_type->id(),
$bundle,
$entity_type->getClass(),
$entity_type->isInternal() || (bool) $resource_config->get('disabled'),
static::isLocatableResourceType($entity_type, $bundle),
TRUE,
$resource_config->getFieldMapping()
);
// Inject additional services through setters. By using setter injection
// rather that constructor injection, we prevent future BC breaks.
$resource_type->setJsonapiResourceConfig($resource_config);
$resource_type->setEnhancerManager($this->enhancerManager);
$resource_type->setConfigFactory($this->configFactory);
return $resource_type;
}
/**
* Injects a additional services into the configurable resource type.
*
......@@ -108,6 +170,8 @@ class ConfigurableResourceTypeRepository extends ResourceTypeRepository {
* The resource type.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
*
* @todo Remove this when JSON API Extras drops support for JSON API 1.x.
*/
protected function injectAdditionalServicesToResourceType(ConfigurableResourceType $resource_type) {
$resource_config_id = sprintf(
......@@ -177,7 +241,7 @@ class ConfigurableResourceTypeRepository extends ResourceTypeRepository {
$entity_type_ids = array_keys($this->entityTypeManager->getDefinitions());
// For each entity type return as many tuples as bundles.
return array_reduce($entity_type_ids, function ($carry, $entity_type_id) {
$bundles = array_keys($this->bundleManager->getBundleInfo($entity_type_id));
$bundles = array_keys($this->entityTypeBundleInfo->getBundleInfo($entity_type_id));
// Get all the tuples for the current entity type.
$tuples = array_map(function ($bundle) use ($entity_type_id) {
return [$entity_type_id, $bundle];
......
<?php
namespace Drupal\jsonapi_extras;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Serializer\Encoder\DecoderInterface;
use Symfony\Component\Serializer\Encoder\EncoderInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Drupal\jsonapi\Serializer\Serializer;
/**
* A decorated JSON API serializer, with lazily initialized fallback serializer.
*/
class SerializerDecorator implements SerializerInterface, NormalizerInterface, DenormalizerInterface, EncoderInterface, DecoderInterface {
/**
* The decorated JSON API serializer service.
*
* @var \Drupal\jsonapi\Serializer\Serializer
*/
protected $decoratedSerializer;
/**
* Whether the lazy dependency has been initialized.
*
* @var bool
*/
protected $isInitialized = FALSE;
/**
* Constructs a SerializerDecorator.
*
* @param \Drupal\jsonapi\Serializer\Serializer $serializer
* The decorated JSON API serializer.
*/
public function __construct(Serializer $serializer) {
$this->decoratedSerializer = $serializer;
}
/**
* Lazily initializes the fallback serializer for the JSON API serializer.
*
* Breaks circular dependency.
*/
protected function lazilyInitialize() {
if (!$this->isInitialized) {
$core_serializer = \Drupal::service('serializer');
$this->decoratedSerializer->setFallbackNormalizer($core_serializer);
$this->isInitialized = TRUE;
}
}
/**
* Relays a method call to the decorated service.
*
* @param string $method_name
* The method to invoke on the decorated serializer.
* @param array $args
* The arguments to pass to the invoked method on the decorated serializer.
*
* @return mixed
* The return value.
*/
protected function relay($method_name, array $args) {
$this->lazilyInitialize();
return call_user_func_array([$this->decoratedSerializer, $method_name], $args);
}
/**
* {@inheritdoc}
*/
public function decode($data, $format, array $context = []) {
return $this->relay(__FUNCTION__, func_get_args());
}
/**
* {@inheritdoc}
*/
public function denormalize($data, $class, $format = NULL, array $context = []) {
return $this->relay(__FUNCTION__, func_get_args());
}
/**
* {@inheritdoc}
*/
public function deserialize($data, $type, $format, array $context = []) {
return $this->relay(__FUNCTION__, func_get_args());
}
/**
* {@inheritdoc}
*/
public function encode($data, $format, array $context = []) {
return $this->relay(__FUNCTION__, func_get_args());
}
/**
* {@inheritdoc}
*/
public function normalize($object, $format = NULL, array $context = []) {
return $this->relay(__FUNCTION__, func_get_args());
}
/**
* {@inheritdoc}
*/
public function supportsDecoding($format) {
return $this->relay(__FUNCTION__, func_get_args());
}
/**
* {@inheritdoc}
*/
public function serialize($data, $format, array $context = []) {
return $this->relay(__FUNCTION__, func_get_args());
}
/**
* {@inheritdoc}
*/