From c6ce6aeea99cb4f48ba7a8d81b1a4357cc1b9202 Mon Sep 17 00:00:00 2001 From: Chi Date: Sun, 4 Apr 2021 16:58:21 +0500 Subject: [PATCH] Add 'cache_metadata' Twig filter --- composer.json | 3 +- docs/cheat-sheet.md | 9 +- src/CacheMetadataExtractor.php | 52 +++++++++++ src/TwigTweakExtension.php | 14 +++ src/UrlExtractor.php | 9 ++ .../src/Kernel/CacheMetadataExtractorTest.php | 91 +++++++++++++++++++ twig_tweak.services.yml | 3 + 7 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 src/CacheMetadataExtractor.php create mode 100644 tests/src/Kernel/CacheMetadataExtractorTest.php diff --git a/composer.json b/composer.json index 53bf28d..c86fed2 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ "php": ">=7.3", "ext-json": "*", "drupal/core": "^9.0", - "twig/twig": "^2.12" + "twig/twig": "^2.12", + "symfony/polyfill-php80": "^2.12" }, "suggest": { "symfony/var-dumper": "Better dump() function for debugging Twig variables" diff --git a/docs/cheat-sheet.md b/docs/cheat-sheet.md index 065945a..dd53de4 100644 --- a/docs/cheat-sheet.md +++ b/docs/cheat-sheet.md @@ -289,7 +289,6 @@ For string arguments it works similar to core `file_url()` Twig function. In order to generate absolute URL set "relative" parameter to `false`. ```twig -{{ 'public://sea.jpg'|file_url(relative=false) }} {{ 'public://sea.jpg'|file_url(false) }} ``` @@ -318,6 +317,14 @@ That is typically needed when printing data from referenced entities. {{ media|translation.title|view }} ``` +## Cache metadata +When using raw values from entities or render arrays it is essential to +ensure that cache metadata are bubbled up. +```twig +Logo +{{ content.field_media|cache_metadata }} +``` + ## PHP PHP filter is disabled by default. You can enable it in `settings.php` file as follows: diff --git a/src/CacheMetadataExtractor.php b/src/CacheMetadataExtractor.php new file mode 100644 index 0000000..ce37e8f --- /dev/null +++ b/src/CacheMetadataExtractor.php @@ -0,0 +1,52 @@ +applyTo($build); + return $build; + } + + /** + * Extracts cache metadata from renders array. + */ + private function extractFromArray(array $build): CacheableMetadata { + $cache_metadata = CacheableMetadata::createFromRenderArray($build); + $keys = Element::children($build); + foreach (array_intersect_key($build, array_flip($keys)) as $item) { + $cache_metadata->addCacheableDependency(self::extractFromArray($item)); + } + return $cache_metadata; + } + +} diff --git a/src/TwigTweakExtension.php b/src/TwigTweakExtension.php index 756a26c..e6516b2 100644 --- a/src/TwigTweakExtension.php +++ b/src/TwigTweakExtension.php @@ -113,6 +113,7 @@ class TwigTweakExtension extends AbstractExtension { new TwigFilter('file_uri', [self::class, 'fileUriFilter']), new TwigFilter('file_url', [self::class, 'fileUrlFilter']), new TwigFilter('translation', [self::class, 'entityTranslation']), + new TwigFilter('cache_metadata', [self::class, 'CacheMetadata']), ]; if (Settings::get('twig_tweak_enable_php_filter')) { @@ -618,6 +619,19 @@ class TwigTweakExtension extends AbstractExtension { return \Drupal::service('entity.repository')->getTranslationFromContext($entity, $langcode); } + /** + * Extracts cache metadata from object or render array. + * + * @param \Drupal\Core\Cache\CacheableDependencyInterface|array $input + * The cacheable object or render array. + * + * @return array + * A render array with extracted cache metadata. + */ + public static function cacheMetadata($input): array { + return \Drupal::service('twig_tweak.cache_metadata_extractor')->extractCacheMetadata($input); + } + /** * Evaluates a string of PHP code. * diff --git a/src/UrlExtractor.php b/src/UrlExtractor.php index a818e5c..1fcb032 100644 --- a/src/UrlExtractor.php +++ b/src/UrlExtractor.php @@ -5,8 +5,11 @@ namespace Drupal\twig_tweak; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Field\EntityReferenceFieldItemListInterface; +use Drupal\Core\Field\FieldItemList; use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem; use Drupal\file\FileInterface; +use Drupal\link\LinkItemInterface; +use Drupal\link\Plugin\Field\FieldType\LinkItem; use Drupal\media\MediaInterface; use Drupal\media\Plugin\media\Source\OEmbedInterface; @@ -56,6 +59,12 @@ class UrlExtractor { elseif ($input instanceof EntityReferenceItem) { return $this->getUrlFromEntity($input->entity, $relative); } + elseif ($input instanceof LinkItemInterface) { + return $input->getUrl()->toString(); + } + elseif ($input instanceof FieldItemList && $input->first() instanceof LinkItemInterface) { + return $input->first()->getUrl()->toString(); + } return NULL; } diff --git a/tests/src/Kernel/CacheMetadataExtractorTest.php b/tests/src/Kernel/CacheMetadataExtractorTest.php new file mode 100644 index 0000000..2416dac --- /dev/null +++ b/tests/src/Kernel/CacheMetadataExtractorTest.php @@ -0,0 +1,91 @@ +container->get('twig_tweak.cache_metadata_extractor'); + + // -- Object. + $input = new CacheableMetadata(); + $input->setCacheMaxAge(5); + $input->setCacheContexts(['url', 'user.permissions']); + $input->setCacheTags(['node', 'node.view']); + + $build = $extractor->extractCacheMetadata($input); + $expected_build['#cache'] = [ + 'contexts' => ['url', 'user.permissions'], + 'tags' => ['node', 'node.view'], + 'max-age' => 5, + ]; + self::assertSame($expected_build, $build); + + // -- Render array. + $input = [ + 'foo' => [ + '#cache' => [ + 'tags' => ['foo', 'foo.view'], + ], + 'bar' => [ + 0 => [ + '#cache' => [ + 'tags' => ['bar-0'], + ], + ], + 1 => [ + '#cache' => [ + 'tags' => ['bar-1'], + ], + ], + '#cache' => [ + 'tags' => ['bar', 'bar.view'], + 'contexts' => ['url.path'], + 'max-age' => 10, + ], + ], + ], + '#cache' => [ + 'contexts' => ['url', 'user.permissions'], + 'tags' => ['node', 'node.view'], + 'max-age' => 20, + ], + ]; + $build = $extractor->extractCacheMetadata($input); + + $expected_build = [ + '#cache' => [ + 'contexts' => ['url', 'url.path', 'user.permissions'], + 'tags' => [ + 'bar', + 'bar-0', + 'bar-1', + 'bar.view', + 'foo', + 'foo.view', + 'node', + 'node.view', + ], + 'max-age' => 10, + ], + ]; + self::assertSame($expected_build, $build); + + // -- Wrong type. + self::expectErrorMessage('The input should be either instance of Drupal\Core\Cache\CacheableDependencyInterface or array. stdClass was given.'); + /** @noinspection PhpParamsInspection */ + $extractor->extractCacheMetadata(new \stdClass()); + } + +} diff --git a/twig_tweak.services.yml b/twig_tweak.services.yml index c9d4f92..71d2365 100644 --- a/twig_tweak.services.yml +++ b/twig_tweak.services.yml @@ -39,3 +39,6 @@ services: twig_tweak.uri_extractor: class: Drupal\twig_tweak\UriExtractor arguments: ['@entity_type.manager'] + + twig_tweak.cache_metadata_extractor: + class: Drupal\twig_tweak\CacheMetadataExtractor