diff --git a/core/modules/image/src/ImageServiceProvider.php b/core/modules/image/src/ImageServiceProvider.php new file mode 100644 index 0000000..de34726 --- /dev/null +++ b/core/modules/image/src/ImageServiceProvider.php @@ -0,0 +1,59 @@ +getParameter('container.modules'); + if (isset($modules['serialization'])) { + // Add an ImageItem normalizer. + $service_definition = new Definition(ImageItemNormalizer::class, [ + new Reference('renderer'), + ]); + // Priority should be higher than + // serializer.normalizer.entity_reference_field_item but lower than + // serializer.normalizer.entity_reference_item.hal. + $service_definition->addTag('normalizer', ['priority' => 9]); + $container->setDefinition('image.normalizer.image_item', $service_definition); + + if (isset($modules['hal'])) { + // Add an ImageItem normalizer. + $service_definition = new Definition(ImageItemHalNormalizer::class, [ + new Reference('hal.link_manager'), + new Reference('serializer.entity_resolver'), + new Reference('renderer'), + ]); + // Priority should be higher than + // serializer.normalizer.entity_reference_item.hal which is 10. + // Priority of 20 gives the ability to have other normalizers between + // this one and serializer.normalizer.entity_reference_item.hal. + $service_definition->addTag('normalizer', ['priority' => 20]); + $container->setDefinition('image.normalizer.hal.image_item', $service_definition); + } + } + } + +} diff --git a/core/modules/image/src/Normalizer/ImageItemHalNormalizer.php b/core/modules/image/src/Normalizer/ImageItemHalNormalizer.php new file mode 100644 index 0000000..941ea94 --- /dev/null +++ b/core/modules/image/src/Normalizer/ImageItemHalNormalizer.php @@ -0,0 +1,58 @@ +renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public function normalize($field_item, $format = NULL, array $context = []) { + /* @var \Drupal\image\Plugin\Field\FieldType\ImageItem $field_item */ + $normalization = parent::normalize($field_item, $format, $context); + if (!$field_item->isEmpty()) { + $field_key = array_keys($normalization['_embedded'])[0]; + $this->decorateWithImageStyles($field_item, $normalization['_embedded'][$field_key][0], $context); + } + return $normalization; + } + +} diff --git a/core/modules/image/src/Normalizer/ImageItemNormalizer.php b/core/modules/image/src/Normalizer/ImageItemNormalizer.php new file mode 100644 index 0000000..93fc3df --- /dev/null +++ b/core/modules/image/src/Normalizer/ImageItemNormalizer.php @@ -0,0 +1,51 @@ +renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public function normalize($field_item, $format = NULL, array $context = []) { + /* @var \Drupal\image\Plugin\Field\FieldType\ImageItem $field_item */ + $normalization = parent::normalize($field_item, $format, $context); + if (!$field_item->isEmpty()) { + $this->decorateWithImageStyles($field_item, $normalization, $context); + } + + return $normalization; + } + +} diff --git a/core/modules/image/src/Normalizer/ImageItemNormalizerTrait.php b/core/modules/image/src/Normalizer/ImageItemNormalizerTrait.php new file mode 100644 index 0000000..e908e5be --- /dev/null +++ b/core/modules/image/src/Normalizer/ImageItemNormalizerTrait.php @@ -0,0 +1,48 @@ +target_id)) { + $uri = $image->getFileUri(); + /** @var \Drupal\image\ImageStyleInterface[] $styles */ + $styles = ImageStyle::loadMultiple(); + $normalization['image_styles'] = []; + foreach ($styles as $id => $style) { + if ($style->supportsUri($uri)) { + $dimensions = ['width' => $item->width, 'height' => $item->height]; + $style->transformDimensions($dimensions, $uri); + $normalization['image_styles'][$id] = [ + 'url' => file_url_transform_relative($style->buildUrl($uri)), + 'height' => empty($dimensions['height']) ? NULL : $dimensions['height'], + 'width' => empty($dimensions['width']) ? NULL : $dimensions['width'], + ]; + if (!empty($context['cacheability'])) { + $context['cacheability']->addCacheableDependency($style); + } + } + } + } + } + +} diff --git a/core/modules/image/tests/src/Kernel/Normalizer/ImageItemHalNormalizerTest.php b/core/modules/image/tests/src/Kernel/Normalizer/ImageItemHalNormalizerTest.php new file mode 100644 index 0000000..7e4cad8 --- /dev/null +++ b/core/modules/image/tests/src/Kernel/Normalizer/ImageItemHalNormalizerTest.php @@ -0,0 +1,28 @@ +serializer = $this->container->get('serializer'); + + $this->installEntitySchema('entity_test'); + $this->installEntitySchema('user'); + $this->installEntitySchema('file'); + $this->installConfig('system'); + $this->installConfig('image'); + $this->installSchema('file', ['file_usage']); + + FieldStorageConfig::create([ + 'entity_type' => 'entity_test', + 'field_name' => 'image_test', + 'type' => 'image', + 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, + ])->save(); + FieldConfig::create([ + 'entity_type' => 'entity_test', + 'field_name' => 'image_test', + 'bundle' => 'entity_test', + 'settings' => [ + 'file_extensions' => 'jpg,svg', + ], + ])->save(); + + // Set upscale to TRUE in all image style scale effects. + /** @var \Drupal\image\Entity\ImageStyle $image_style */ + foreach (ImageStyle::loadMultiple() as $image_style) { + foreach ($image_style->getEffects() as $effect) { + $config = $effect->getConfiguration(); + $config['data']['upscale'] = TRUE; + $effect->setConfiguration($config); + } + $image_style->save(); + } + + // Create the images needed for tests. + // @see ::imagesProvider() + file_unmanaged_copy(\Drupal::root() . '/core/misc/druplicon.png', 'public://example.jpg'); + + // We don't have to put any real SVG data in here, because the GD toolkit + // won't be able to load it anyway. + touch('public://example.svg'); + } + + /** + * Test that the decorator provides additional image style information. + * + * @see \Drupal\image\Normalizer\ImageItemNormalizerTrait::decorateWithImageStyles() + * + * @covers ::normalize + * + * @dataProvider imagesProvider + */ + public function testNormalize($uri, $styles_included) { + + $image = File::create([ + 'uri' => $uri, + ]); + $image->save(); + // Create a test entity with the image field set. + $original_entity = EntityTest::create(); + $original_entity->image_test->target_id = $image->id(); + $original_entity->image_test->alt = $alt = $this->randomMachineName(); + $original_entity->image_test->title = $title = $this->randomMachineName(); + $original_entity->name->value = $this->randomMachineName(); + $original_entity->save(); + + $entity = clone $original_entity; + $cacheable_metadata = new CacheableMetadata(); + $normalization = $this->serializer->normalize($entity, $this->format, ['cacheability' => $cacheable_metadata]); + + $normalized_image_styles = $this->getNormalizedImageStyles($normalization); + if ($styles_included) { + $this->assertEquals($this->getExpectedCacheability($styles_included), $cacheable_metadata); + $expect_dimensions = [ + 'large' => [ + 'width' => '422', + 'height' => '480', + ], + 'medium' => [ + 'width' => '194', + 'height' => '220', + ], + 'thumbnail' => [ + 'width' => '88', + 'height' => '100', + ], + ]; + $this->assertEquals(array_keys($expect_dimensions), array_keys($normalized_image_styles)); + + foreach ($normalized_image_styles as $image_style_id => $image_style_dimensions) { + $this->assertContains("files/styles/$image_style_id/public/example.jpg", $image_style_dimensions['url']); + $this->assertEquals($expect_dimensions[$image_style_id]['height'], $image_style_dimensions['height'], "Style $image_style_id matches height."); + $this->assertEquals($expect_dimensions[$image_style_id]['width'], $image_style_dimensions['width'], "Style $image_style_id matches width."); + } + } + else { + $this->assertEquals([], $normalized_image_styles); + } + + } + + /** + * Gets normalized image styles from normalized entity. + * + * @param array $normalization + * The normalized entity. + * + * @return array + * The normalized image styles. + */ + abstract protected function getNormalizedImageStyles(array $normalization); + + /** + * Gets the expected bubbled cacheability metadata. + * + * @param bool $styles_included + * Whether styles will be included in normalization. + * + * @return \Drupal\Core\Cache\CacheableMetadata + * The expected cacheability metadata. + */ + protected function getExpectedCacheability($styles_included) { + $cacheability = new CacheableMetadata(); + $cache_tags = []; + if ($styles_included) { + /** @var \Drupal\image\ImageStyleInterface $image_style */ + foreach (ImageStyle::loadMultiple() as $image_style) { + $cache_tags = Cache::mergeTags($cache_tags, $image_style->getCacheTags()); + } + } + $cacheability->setCacheTags($cache_tags); + return $cacheability; + } + + /** + * Datatprovider for testNormalize(). + */ + public function imagesProvider() { + return [ + 'jpg' => [ + 'uri' => 'public://example.jpg', + 'styles_included' => TRUE, + ], + // Add a test case for svg file because images styles will not be + // included. + 'svg' => [ + 'uri' => 'public://example.svg', + 'styles_included' => FALSE, + ], + ]; + } + +} diff --git a/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php b/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php index 5cb887b..fcade94 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; @@ -126,11 +127,18 @@ 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 prior versions of Drupal 8, we allowed implicit bubbling of cacheability + * metadata 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. @@ -150,14 +158,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, + '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['cacheability']); } $response->setContent($output);