diff --git a/core/modules/hal/tests/src/Functional/EntityResource/EntityTest/EntityTestHalJsonInternalPropertyNormalizerTest.php b/core/modules/hal/tests/src/Functional/EntityResource/EntityTest/EntityTestHalJsonInternalPropertyNormalizerTest.php index 8ee32155712cc740fccf28547de4d73e165c8e71..819a125f82ab6e6175ee35d7b8231fa31f4fcf3c 100644 --- a/core/modules/hal/tests/src/Functional/EntityResource/EntityTest/EntityTestHalJsonInternalPropertyNormalizerTest.php +++ b/core/modules/hal/tests/src/Functional/EntityResource/EntityTest/EntityTestHalJsonInternalPropertyNormalizerTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\hal\Functional\EntityResource\EntityTest; +use Drupal\Core\Cache\Cache; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\Tests\hal\Functional\EntityResource\HalEntityNormalizationTrait; @@ -82,4 +83,18 @@ protected function getNormalizedPostEntity() { ]; } + /** + * {@inheritdoc} + */ + protected function getExpectedCacheContexts() { + return Cache::mergeContexts(parent::getExpectedCacheContexts(), ['request_format']); + } + + /** + * {@inheritdoc} + */ + protected function getExpectedCacheTags() { + return Cache::mergeTags(parent::getExpectedCacheTags(), ['you_are_it', 'no_tag_backs']); + } + } diff --git a/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php b/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php index ef33817ce85c3fb6a98bdb12eb18307117c11605..babb238b4561107b5b785130f2b0ced4ddc16e70 100644 --- a/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php +++ b/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php @@ -2,6 +2,7 @@ namespace Drupal\rest\EventSubscriber; +use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Cache\CacheableResponse; use Drupal\Core\Cache\CacheableResponseInterface; use Drupal\Core\Render\RenderContext; @@ -20,6 +21,15 @@ */ class ResourceResponseSubscriber implements EventSubscriberInterface { + /** + * Name of key for bubbling cacheability metadata via serialization context. + * + * @see \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize() + * @see \Symfony\Component\Serializer\SerializerInterface::serialize() + * @see \Drupal\rest\EventSubscriber\ResourceResponseSubscriber::renderResponseBody() + */ + const SERIALIZATION_CONTEXT_CACHEABILITY = 'cacheability'; + /** * The serializer. * @@ -128,11 +138,19 @@ public function getResponseFormat(RouteMatchInterface $route_match, Request $req /** * Renders a resource response body. * - * Serialization can invoke rendering (e.g., generating URLs), but the - * serialization API does not provide a mechanism to collect the - * bubbleable metadata associated with that (e.g., language and other - * contexts), so instead, allow those to "leak" and collect them here in - * a render context. + * During serialization, encoders and normalizers are able to explicitly + * bubble cacheability metadata via the 'cacheability' key-value pair in the + * received context. This bubbled cacheability metadata will be applied to the + * the response. + * + * In versions of Drupal prior to 8.5, implicit bubbling of cacheability + * metadata was allowed because there was no explicit cacheability metadata + * bubbling API. To maintain backwards compatibility, we continue to support + * this, but support for this will be dropped in Drupal 9.0.0. This is + * especially useful when interacting with APIs that implicitly invoke + * rendering (for example: generating URLs): this allows those to "leak", and + * we collect their bubbled cacheability metadata automatically in a render + * context. * * @param \Symfony\Component\HttpFoundation\Request $request * The request object. @@ -152,14 +170,25 @@ protected function renderResponseBody(Request $request, ResourceResponseInterfac // If there is data to send, serialize and set it as the response body. if ($data !== NULL) { + $serialization_context = [ + 'request' => $request, + static::SERIALIZATION_CONTEXT_CACHEABILITY => new CacheableMetadata(), + ]; + + // @deprecated In Drupal 8.5.0, will be removed before Drupal 9.0.0. Use + // explicit cacheability metadata bubbling instead. (The wrapping call to + // executeInRenderContext() will be removed before Drupal 9.0.0.) $context = new RenderContext(); $output = $this->renderer - ->executeInRenderContext($context, function () use ($serializer, $data, $format) { - return $serializer->serialize($data, $format); + ->executeInRenderContext($context, function () use ($serializer, $data, $format, $serialization_context) { + return $serializer->serialize($data, $format, $serialization_context); }); - - if ($response instanceof CacheableResponseInterface && !$context->isEmpty()) { - $response->addCacheableDependency($context->pop()); + if ($response instanceof CacheableResponseInterface) { + if (!$context->isEmpty()) { + @trigger_error('Implicit cacheability metadata bubbling (onto the global render context) in normalizers is deprecated since Drupal 8.5.0 and will be removed in Drupal 9.0.0. Use the "cacheability" serialization context instead, for explicit cacheability metadata bubbling. See https://www.drupal.org/node/2918937', E_USER_DEPRECATED); + $response->addCacheableDependency($context->pop()); + } + $response->addCacheableDependency($serialization_context[static::SERIALIZATION_CONTEXT_CACHEABILITY]); } $response->setContent($output); diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestJsonInternalPropertyNormalizerTest.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestJsonInternalPropertyNormalizerTest.php index c369e17050fc88a24f97be7d3b33f2e042877f4d..1944718cf7136e2a67a8a19aa5426ddc093dc3dd 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestJsonInternalPropertyNormalizerTest.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestJsonInternalPropertyNormalizerTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\rest\Functional\EntityResource\EntityTest; +use Drupal\Core\Cache\Cache; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\Tests\rest\Functional\AnonResourceTestTrait; @@ -84,4 +85,18 @@ protected function getNormalizedPostEntity() { ]; } + /** + * {@inheritdoc} + */ + protected function getExpectedCacheContexts() { + return Cache::mergeContexts(parent::getExpectedCacheContexts(), ['request_format']); + } + + /** + * {@inheritdoc} + */ + protected function getExpectedCacheTags() { + return Cache::mergeTags(parent::getExpectedCacheTags(), ['you_are_it', 'no_tag_backs']); + } + } diff --git a/core/modules/serialization/src/Normalizer/NormalizerBase.php b/core/modules/serialization/src/Normalizer/NormalizerBase.php index 5e829f65e701a0b7b3f2c2a24713fbe1cc0e9691..958aaf2f4c03738a0a06766457476e7e3211400a 100644 --- a/core/modules/serialization/src/Normalizer/NormalizerBase.php +++ b/core/modules/serialization/src/Normalizer/NormalizerBase.php @@ -2,6 +2,8 @@ namespace Drupal\serialization\Normalizer; +use Drupal\Core\Cache\CacheableDependencyInterface; +use Drupal\rest\EventSubscriber\ResourceResponseSubscriber; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\SerializerAwareNormalizer; @@ -81,4 +83,18 @@ protected function checkFormat($format = NULL) { return in_array($format, (array) $this->format, TRUE); } + /** + * Adds cacheability if applicable. + * + * @param array $context + * Context options for the normalizer. + * @param $data + * The data that might have cacheability information. + */ + protected function addCacheableDependency(array $context, $data) { + if ($data instanceof CacheableDependencyInterface && isset($context[ResourceResponseSubscriber::SERIALIZATION_CONTEXT_CACHEABILITY])) { + $context[ResourceResponseSubscriber::SERIALIZATION_CONTEXT_CACHEABILITY]->addCacheableDependency($data); + } + } + } diff --git a/core/modules/serialization/src/Normalizer/TypedDataNormalizer.php b/core/modules/serialization/src/Normalizer/TypedDataNormalizer.php index 60ce7d08f696547ed1fb4ab8211aeb80fc715e19..958b987dc7daa070f33780496a58d820f2be0250 100644 --- a/core/modules/serialization/src/Normalizer/TypedDataNormalizer.php +++ b/core/modules/serialization/src/Normalizer/TypedDataNormalizer.php @@ -18,6 +18,7 @@ class TypedDataNormalizer extends NormalizerBase { * {@inheritdoc} */ public function normalize($object, $format = NULL, array $context = []) { + $this->addCacheableDependency($context, $object); return $object->getValue(); } diff --git a/core/modules/system/tests/modules/entity_test/src/TypedData/ComputedString.php b/core/modules/system/tests/modules/entity_test/src/TypedData/ComputedString.php index 121699c8071cc385b1d067f38cf514f4cd848fd7..a8175263b37307448ca9458886317b26b8225548 100644 --- a/core/modules/system/tests/modules/entity_test/src/TypedData/ComputedString.php +++ b/core/modules/system/tests/modules/entity_test/src/TypedData/ComputedString.php @@ -2,12 +2,14 @@ namespace Drupal\entity_test\TypedData; +use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheableDependencyInterface; use Drupal\Core\TypedData\TypedData; /** * A computed property for test strings. */ -class ComputedString extends TypedData { +class ComputedString extends TypedData implements CacheableDependencyInterface { /** * {@inheritdoc} @@ -27,4 +29,25 @@ public function getCastedValue() { return $this->getString(); } + /** + * {@inheritdoc} + */ + public function getCacheTags() { + return ['you_are_it', 'no_tag_backs']; + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return ['request_format']; + } + + /** + * {@inheritdoc} + */ + public function getCacheMaxAge() { + return Cache::PERMANENT; + } + }