.../hal/src/LinkManager/LinkManagerBase.php | 23 ++++- .../hal/src/LinkManager/RelationLinkManager.php | 2 +- .../hal/src/LinkManager/TypeLinkManager.php | 2 +- .../hal/tests/src/Kernel/HalLinkManagerTest.php | 98 +++++++++++++++++++--- .../EventSubscriber/ResourceResponseSubscriber.php | 49 ++++++++--- 5 files changed, 149 insertions(+), 25 deletions(-) diff --git a/core/modules/hal/src/LinkManager/LinkManagerBase.php b/core/modules/hal/src/LinkManager/LinkManagerBase.php index 20adc08..b6af162 100644 --- a/core/modules/hal/src/LinkManager/LinkManagerBase.php +++ b/core/modules/hal/src/LinkManager/LinkManagerBase.php @@ -2,6 +2,8 @@ namespace Drupal\hal\LinkManager; +use Drupal\rest\EventSubscriber\ResourceResponseSubscriber; + /** * Defines an abstract base-class for HAL link manager objects. */ @@ -39,17 +41,32 @@ public function setLinkDomain($domain) { /** * Gets the link domain. * + * @param array $context + * Normalization/serialization context. + * * @return string * The link domain. + * + * @see \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize() + * @see \Symfony\Component\Serializer\SerializerInterface::serialize() + * @see \Drupal\rest\EventSubscriber\ResourceResponseSubscriber::SERIALIZATION_CONTEXT_CACHEABILITY */ - protected function getLinkDomain() { + protected function getLinkDomain(array $context = []) { if (empty($this->linkDomain)) { if ($domain = $this->configFactory->get('hal.settings')->get('link_domain')) { - $this->linkDomain = rtrim($domain, '/'); + // Bubble the appropriate cacheability metadata whenever possible. + if (isset($context[ResourceResponseSubscriber::SERIALIZATION_CONTEXT_CACHEABILITY])) { + $context[ResourceResponseSubscriber::SERIALIZATION_CONTEXT_CACHEABILITY]->addCacheableDependency($this->configFactory->get('hal.settings')); + } + return rtrim($domain, '/'); } else { + // Bubble the relevant cacheability metadata whenever possible. + if (isset($context[ResourceResponseSubscriber::SERIALIZATION_CONTEXT_CACHEABILITY])) { + $context[ResourceResponseSubscriber::SERIALIZATION_CONTEXT_CACHEABILITY]->addCacheContexts(['url.site']); + } $request = $this->requestStack->getCurrentRequest(); - $this->linkDomain = $request->getSchemeAndHttpHost() . $request->getBasePath(); + return $request->getSchemeAndHttpHost() . $request->getBasePath(); } } return $this->linkDomain; diff --git a/core/modules/hal/src/LinkManager/RelationLinkManager.php b/core/modules/hal/src/LinkManager/RelationLinkManager.php index a33b6c3..ec63141 100644 --- a/core/modules/hal/src/LinkManager/RelationLinkManager.php +++ b/core/modules/hal/src/LinkManager/RelationLinkManager.php @@ -68,7 +68,7 @@ public function getRelationUri($entity_type, $bundle, $field_name, $context = [] // module is installed that adds such content, but requires this URL to be // different (e.g., include a language prefix), then the module must also // override the RelationLinkManager class/service to return the desired URL. - $uri = $this->getLinkDomain() . "/rest/relation/$entity_type/$bundle/$field_name"; + $uri = $this->getLinkDomain($context) . "/rest/relation/$entity_type/$bundle/$field_name"; $this->moduleHandler->alter('hal_relation_uri', $uri, $context); // @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0. This // hook is invoked to maintain backwards compatibility diff --git a/core/modules/hal/src/LinkManager/TypeLinkManager.php b/core/modules/hal/src/LinkManager/TypeLinkManager.php index 51b2de5..47206d8 100644 --- a/core/modules/hal/src/LinkManager/TypeLinkManager.php +++ b/core/modules/hal/src/LinkManager/TypeLinkManager.php @@ -69,7 +69,7 @@ public function getTypeUri($entity_type, $bundle, $context = []) { // installed that adds such content, but requires this URL to be different // (e.g., include a language prefix), then the module must also override the // TypeLinkManager class/service to return the desired URL. - $uri = $this->getLinkDomain() . "/rest/type/$entity_type/$bundle"; + $uri = $this->getLinkDomain($context) . "/rest/type/$entity_type/$bundle"; $this->moduleHandler->alter('hal_type_uri', $uri, $context); // @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0. This // hook is invoked to maintain backwards compatibility diff --git a/core/modules/hal/tests/src/Kernel/HalLinkManagerTest.php b/core/modules/hal/tests/src/Kernel/HalLinkManagerTest.php index 098868e..975d8a1 100644 --- a/core/modules/hal/tests/src/Kernel/HalLinkManagerTest.php +++ b/core/modules/hal/tests/src/Kernel/HalLinkManagerTest.php @@ -2,11 +2,13 @@ namespace Drupal\Tests\hal\Kernel; +use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Url; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\KernelTests\KernelTestBase; use Drupal\node\Entity\NodeType; +use Drupal\rest\EventSubscriber\ResourceResponseSubscriber; /** * @coversDefaultClass \Drupal\hal\LinkManager\LinkManager @@ -46,19 +48,95 @@ protected function setUp() { /** * @covers ::getTypeUri + * @dataProvider providerTestGetTypeUri */ - public function testGetTypeUri() { + public function testGetTypeUri($link_domain, $entity_type, $bundle, array $context, $expected_return, array $expected_context) { + $hal_settings = \Drupal::configFactory()->getEditable('hal.settings'); + + if ($link_domain === NULL) { + $hal_settings->clear('link_domain'); + } + else { + $hal_settings->set('link_domain', $link_domain)->save(TRUE); + } + /* @var \Drupal\rest\LinkManager\TypeLinkManagerInterface $type_manager */ $type_manager = \Drupal::service('hal.link_manager.type'); - $base = Url::fromRoute('', [], ['absolute' => TRUE])->toString(); - $link = $type_manager->getTypeUri('node', 'page'); - $this->assertSame($link, $base . 'rest/type/node/page'); - // Now with optional context. - $link = $type_manager->getTypeUri('node', 'page', ['hal_test' => TRUE]); - $this->assertSame($link, 'hal_test_type'); - // Test BC: hook_rest_type_uri_alter(). - $link = $type_manager->getTypeUri('node', 'page', ['rest_test' => TRUE]); - $this->assertSame($link, 'rest_test_type'); + + $link = $type_manager->getTypeUri($entity_type, $bundle, $context); + $this->assertSame($link, str_replace('BASE_URL/', Url::fromRoute('', [], ['absolute' => TRUE])->toString(), $expected_return)); + $this->assertEquals($context, $expected_context); + } + + public function providerTestGetTypeUri() { + $serialization_context_collecting_cacheability = [ + ResourceResponseSubscriber::SERIALIZATION_CONTEXT_CACHEABILITY => new CacheableMetadata() + ]; + $expected_serialization_context_cacheability_url_site = [ + ResourceResponseSubscriber::SERIALIZATION_CONTEXT_CACHEABILITY => (new CacheableMetadata())->setCacheContexts(['url.site']) + ]; + + $base_test_case = [ + 'link_domain' => NULL, + 'entity_type' => 'node', + 'bundle' => 'page', + ]; + + return [ + 'site URL' => $base_test_case + [ + 'context' => [], + 'link_domain' => NULL, + 'expected return' => 'BASE_URL/rest/type/node/page', + 'expected context' => [], + ], + 'site URL, with optional context to collect cacheability metadata' => $base_test_case + [ + 'context' => $serialization_context_collecting_cacheability, + 'expected return' => 'BASE_URL/rest/type/node/page', + 'expected context' => $expected_serialization_context_cacheability_url_site, + ], + // Test hook_hal_type_uri_alter(). + 'site URL, with optional context, to test hook_hal_type_uri_alter()' => $base_test_case + [ + 'context' => ['hal_test' => TRUE], + 'expected return' => 'hal_test_type', + 'expected context' => ['hal_test' => TRUE], + ], + 'site URL, with optional context, to test hook_hal_type_uri_alter(), and collecting cacheability metadata' => $base_test_case + [ + 'context' => ['hal_test' => TRUE] + $serialization_context_collecting_cacheability, + 'expected return' => 'hal_test_type', + // No cacheability metadata bubbled. + 'expected context' => ['hal_test' => TRUE] + $serialization_context_collecting_cacheability, + ], + // Test hook_rest_type_uri_alter() — for backwards compatibility. + 'site URL, with optional context, to test hook_rest_type_uri_alter()' => $base_test_case + [ + 'context' => ['rest_test' => TRUE], + 'expected return' => 'rest_test_type', + 'expected context' => ['rest_test' => TRUE], + ], + 'site URL, with optional context, to test hook_rest_type_uri_alter(), and collecting cacheability metadata' => $base_test_case + [ + 'context' => ['rest_test' => TRUE] + $serialization_context_collecting_cacheability, + 'expected return' => 'rest_test_type', + // No cacheability metadata bubbled. + 'expected context' => ['rest_test' => TRUE] + $serialization_context_collecting_cacheability, + ], + 'configured URL' => [ + 'link_domain' => 'http://llamas-rock.com/for-real/', + 'entity_type' => 'node', + 'bundle' => 'page', + 'context' => [], + 'expected return' => 'http://llamas-rock.com/for-real/rest/type/node/page', + 'expected context' => [], + ], + 'configured URL, with optional context to collect cacheability metadata' => [ + 'link_domain' => 'http://llamas-rock.com/for-real/', + 'entity_type' => 'node', + 'bundle' => 'page', + 'context' => $serialization_context_collecting_cacheability, + 'expected return' => 'http://llamas-rock.com/for-real/rest/type/node/page', + 'expected context' => [ + 'cacheability' => (new CacheableMetadata())->setCacheTags(['config:hal.settings']), + ], + ], + ]; } /** diff --git a/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php b/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php index ef33817..30dc950 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; @@ -21,6 +22,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. * * @var \Symfony\Component\Serializer\SerializerInterface @@ -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.4.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.4.0 and will be removed in Drupal 9.0.0. Use the "cacheability" serialization context instead, for explicit cacheability metadata bubbling.', E_USER_DEPRECATED); + $response->addCacheableDependency($context->pop()); + } + $response->addCacheableDependency($serialization_context[static::SERIALIZATION_CONTEXT_CACHEABILITY]); } $response->setContent($output);