jsonapi.services.yml | 2 +- src/Normalizer/EntityNormalizer.php | 108 +++++++++++++++++++++++++++++++----- 2 files changed, 94 insertions(+), 16 deletions(-) diff --git a/jsonapi.services.yml b/jsonapi.services.yml index 8761daa..47f0e36 100644 --- a/jsonapi.services.yml +++ b/jsonapi.services.yml @@ -66,7 +66,7 @@ services: - { name: jsonapi_normalizer_do_not_use_removal_imminent } serializer.normalizer.entity.jsonapi: class: Drupal\jsonapi\Normalizer\ContentEntityNormalizer - arguments: ['@jsonapi.link_manager', '@jsonapi.resource_type.repository', '@entity_type.manager', '@entity_field.manager', '@plugin.manager.field.field_type', '@cache.jsonapi_normalizations'] + arguments: ['@jsonapi.link_manager', '@jsonapi.resource_type.repository', '@entity_type.manager', '@entity_field.manager', '@plugin.manager.field.field_type', '@render_cache'] tags: - { name: jsonapi_normalizer_do_not_use_removal_imminent } serializer.normalizer.entity.label_only.jsonapi: diff --git a/src/Normalizer/EntityNormalizer.php b/src/Normalizer/EntityNormalizer.php index 8bb486d..9e72c3d 100644 --- a/src/Normalizer/EntityNormalizer.php +++ b/src/Normalizer/EntityNormalizer.php @@ -2,15 +2,14 @@ namespace Drupal\jsonapi\Normalizer; -use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheableMetadata; -use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\FieldTypePluginManagerInterface; +use Drupal\Core\Render\RenderCacheInterface; use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper; use Drupal\jsonapi\Normalizer\Value\CacheableDependenciesMergerTrait; use Drupal\jsonapi\Normalizer\Value\CacheableNormalization; @@ -81,11 +80,11 @@ class EntityNormalizer extends NormalizerBase implements DenormalizerInterface { protected $pluginManager; /** - * Cache backend. + * The render cache. * - * @var \Drupal\Core\Cache\CacheBackendInterface + * @var \Drupal\Core\Render\RenderCacheInterface */ - protected $cache; + protected $renderCache; /** * Constructs an EntityNormalizer object. @@ -100,16 +99,16 @@ class EntityNormalizer extends NormalizerBase implements DenormalizerInterface { * The entity field manager. * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $plugin_manager * The plugin manager for fields. - * @param \Drupal\Core\Cache\CacheBackendInterface $cache - * The cache backend. + * @param \Drupal\Core\Render\RenderCacheInterface $render_cache + * The render cache. */ - public function __construct(LinkManager $link_manager, ResourceTypeRepositoryInterface $resource_type_repository, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $field_manager, FieldTypePluginManagerInterface $plugin_manager, CacheBackendInterface $cache) { + public function __construct(LinkManager $link_manager, ResourceTypeRepositoryInterface $resource_type_repository, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $field_manager, FieldTypePluginManagerInterface $plugin_manager, RenderCacheInterface $render_cache) { $this->linkManager = $link_manager; $this->resourceTypeRepository = $resource_type_repository; $this->entityTypeManager = $entity_type_manager; $this->fieldManager = $field_manager; $this->pluginManager = $plugin_manager; - $this->cache = $cache; + $this->renderCache = $render_cache; } /** @@ -123,16 +122,13 @@ class EntityNormalizer extends NormalizerBase implements DenormalizerInterface { ); $resource_type_name = $resource_type->getTypeName(); - $cid = $resource_type_name . '/' . $entity->uuid(); - $cached_normalization_parts = $this->cache->get($cid); + $cached_normalization_parts = $this->cacheGet($resource_type, $entity); if ($cached_normalization_parts !== FALSE) { - $normalization_parts = $cached_normalization_parts->data; + $normalization_parts = $cached_normalization_parts; } else { $normalization_parts = $this->getPartialNormalization($entity, $resource_type, $format, $context); - $normalization_parts_cacheability = static::mergeCacheableDependencies($normalization_parts['fields']); - $cache_tags = Cache::mergeTags($entity->getCacheTags(), $normalization_parts_cacheability->getCacheTags()); - $this->cache->set($cid, $normalization_parts, Cache::PERMANENT, $cache_tags); + $this->cacheSet($resource_type, $entity, $normalization_parts); } // If a sparse fieldset is active during this normalization, keep only the @@ -196,6 +192,88 @@ class EntityNormalizer extends NormalizerBase implements DenormalizerInterface { } /** + * Generates a lookup render array for a partial normalization. + * + * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type + * The resource type for which to generate a cache item. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity for which to generate a cache item. + * + * @return array + * A render array for use with the RenderCache service. + * + * @see \Drupal\dynamic_page_cache\EventSubscriber\DynamicPageCacheSubscriber::$dynamicPageCacheRedirectRenderArray + */ + protected static function generateLookupRenderArray(ResourceType $resource_type, EntityInterface $entity) { + return [ + '#cache' => [ + 'keys' => [$resource_type->getTypeName(), $entity->uuid()], + 'bin' => 'jsonapi_normalizations', + ], + ]; + } + + /** + * Reads a partial normalization from cache. + * + * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type + * The resource type for which to generate a cache item. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity for which to generate a cache item. + * @return array|FALSE + * The cached normalization parts, or FALSE if not yet cached. + * + * @see \Drupal\dynamic_page_cache\EventSubscriber\DynamicPageCacheSubscriber::renderArrayToResponse() + * @todo Refactor/remove once https://www.drupal.org/node/2551419 lands. + */ + protected function cacheGet(ResourceType $resource_type, EntityInterface $entity) { + $cached = $this->renderCache->get(static::generateLookupRenderArray($resource_type, $entity)); + if ($cached) { + return $cached['#data']; + } + else { + return FALSE; + } + } + + /** + * Writes a partial normalization to cache. + * + * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type + * The resource type for which to generate a cache item. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity for which to generate a cache item. + * @param array $normalization_parts + * The normalization parts to cache. + * + * @see \Drupal\dynamic_page_cache\EventSubscriber\DynamicPageCacheSubscriber::responseToRenderArray() + * @todo Refactor/remove once https://www.drupal.org/node/2551419 lands. + */ + protected function cacheSet(ResourceType $resource_type, EntityInterface $entity, array $normalization_parts) { + assert(array_keys($normalization_parts) === ['base', 'fields']); + $base = static::generateLookupRenderArray($resource_type, $entity); + $data_as_render_array = $base + [ + // The data we actually care about. + '#data' => $normalization_parts, + // Tell RenderCache to cache the #data property: the data we actually care + // about. + '#cache_properties' => ['#data'], + // These exist only to fulfill the requirements of the RenderCache, which + // is designed to work with render arrays only. We don't care about these. + '#markup' => '', + '#attached' => '', + ]; + + // Merge the entity's cacheability metadata with that of the normalization + // parts, so that RenderCache can take care of cache redirects for us. + CacheableMetadata::createFromObject($entity) + ->merge(static::mergeCacheableDependencies($normalization_parts['fields'])) + ->applyTo($data_as_render_array); + + $this->renderCache->set($data_as_render_array, $base); + } + + /** * {@inheritdoc} */ public function denormalize($data, $class, $format = NULL, array $context = []) {