diff --git a/CHANGELOG b/CHANGELOG index 39040b1a8..7b222909e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.29.0 (2026-XX-XX) + * Fix `{% cache %}` always missing in the Symfony bundle when `framework.cache.app` uses a natively tag aware adapter * Fix the sandbox resolving `use` trait templates before checking that the `use` tag is allowed * Fix `html_attr` dropping `style` declarations whose value is `0`, `0.0` or `'0'` * Fix the `default` filter fallback emitting an undefined variable warning when it uses the null-safe operator diff --git a/extra/twig-extra-bundle/DependencyInjection/Compiler/TwigCachePoolPass.php b/extra/twig-extra-bundle/DependencyInjection/Compiler/TwigCachePoolPass.php new file mode 100644 index 000000000..a5bb216fd --- /dev/null +++ b/extra/twig-extra-bundle/DependencyInjection/Compiler/TwigCachePoolPass.php @@ -0,0 +1,57 @@ +hasDefinition('twig.cache') || !$container->hasDefinition('.twig.cache.inner')) { + return; + } + + $definition = $container->getDefinition('.twig.cache.inner'); + $class = $definition->getClass(); + + while (null === $class && $definition instanceof ChildDefinition) { + $parent = $definition->getParent(); + + if (!$container->hasDefinition($parent) && !$container->hasAlias($parent)) { + return; + } + + $definition = $container->findDefinition($parent); + $class = $definition->getClass(); + } + + if (!is_a($class ?? '', TagAwareAdapterInterface::class, true)) { + return; + } + + $container->removeDefinition('twig.cache'); + $container->setAlias('twig.cache', '.twig.cache.inner'); + } +} diff --git a/extra/twig-extra-bundle/Tests/DependencyInjection/Compiler/TwigCachePoolPassTest.php b/extra/twig-extra-bundle/Tests/DependencyInjection/Compiler/TwigCachePoolPassTest.php new file mode 100644 index 000000000..559215ab1 --- /dev/null +++ b/extra/twig-extra-bundle/Tests/DependencyInjection/Compiler/TwigCachePoolPassTest.php @@ -0,0 +1,60 @@ +kernels as $kernel) { + (new Filesystem())->remove($kernel->getTempDir()); + } + + $this->kernels = []; + } + + public function testThePoolIsDecoratedWhenTheAppAdapterIsNotTagAware(): void + { + $pools = $this->compileFor(null); + + $this->assertSame(TagAwareAdapter::class, $pools['twig.cache']); + $this->assertSame(FilesystemAdapter::class, $pools['.twig.cache.inner']); + $this->assertNotSame($pools['cache.app.namespace'], $pools['.twig.cache.inner.namespace']); + } + + public function testThePoolIsNotDecoratedWhenTheAppAdapterIsTagAware(): void + { + $pools = $this->compileFor('cache.adapter.redis_tag_aware'); + + $this->assertSame('@.twig.cache.inner', $pools['twig.cache']); + $this->assertSame(RedisTagAwareAdapter::class, $pools['.twig.cache.inner']); + $this->assertNotSame($pools['cache.app.namespace'], $pools['.twig.cache.inner.namespace']); + } + + private function compileFor(?string $cacheAdapter): array + { + $this->kernels[] = $kernel = new CacheKernel($cacheAdapter); + $kernel->boot(); + + return $kernel->getContainer()->getParameter('twig_extra.test.cache_pools'); + } +} diff --git a/extra/twig-extra-bundle/Tests/Fixture/CacheKernel.php b/extra/twig-extra-bundle/Tests/Fixture/CacheKernel.php new file mode 100644 index 000000000..18dc54c86 --- /dev/null +++ b/extra/twig-extra-bundle/Tests/Fixture/CacheKernel.php @@ -0,0 +1,129 @@ +dir = sys_get_temp_dir().'/twig-extra-bundle/'.bin2hex(random_bytes(6)); + + parent::__construct('test', false); + } + + public function getCacheDir(): string + { + return $this->dir.'/cache'; + } + + public function getLogDir(): string + { + return $this->dir.'/log'; + } + + public function getProjectDir(): string + { + return __DIR__; + } + + public function getTempDir(): string + { + return $this->dir; + } + + public function registerBundles(): iterable + { + yield new FrameworkBundle(); + yield new TwigBundle(); + yield new TwigExtraBundle(); + } + + protected function build(ContainerBuilder $container): void + { + $container->addCompilerPass(new class implements CompilerPassInterface { + public function process(ContainerBuilder $container): void + { + $pools = []; + foreach (['twig.cache', '.twig.cache.inner', 'cache.app'] as $id) { + if ($container->hasAlias($id)) { + $pools[$id] = '@'.$container->getAlias($id); + + continue; + } + + $definition = $container->getDefinition($id); + $pools[$id] = $definition->getClass(); + + // cache adapters take their namespace as their first string argument + foreach ($definition->getArguments() as $argument) { + if (\is_string($argument)) { + $pools[$id.'.namespace'] = $argument; + + break; + } + } + } + + $container->setParameter('twig_extra.test.cache_pools', $pools); + } + }, PassConfig::TYPE_BEFORE_REMOVING, -1000); + } + + protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader): void + { + $config = [ + 'secret' => 'S3CRET', + 'router' => ['utf8' => true], + 'http_method_override' => false, + 'php_errors' => [ + 'log' => true, + ], + ]; + + // the "handle_all_throwables" option was introduced in FrameworkBundle 6.2 (and so was the NotificationAssertionsTrait) + if (trait_exists(NotificationAssertionsTrait::class)) { + $config['handle_all_throwables'] = true; + } + + if (null !== $this->cacheAdapter) { + $config['cache'] = ['app' => $this->cacheAdapter]; + } + + $c->loadFromExtension('framework', $config); + $c->loadFromExtension('twig', [ + 'default_path' => __DIR__.'/views', + ]); + } + + protected function configureRoutes($routes): void + { + } +} diff --git a/extra/twig-extra-bundle/TwigExtraBundle.php b/extra/twig-extra-bundle/TwigExtraBundle.php index 495785623..3475eefc2 100644 --- a/extra/twig-extra-bundle/TwigExtraBundle.php +++ b/extra/twig-extra-bundle/TwigExtraBundle.php @@ -11,10 +11,12 @@ namespace Twig\Extra\TwigExtraBundle; +use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\KernelInterface; use Twig\Extra\TwigExtraBundle\DependencyInjection\Compiler\MissingExtensionSuggestorPass; +use Twig\Extra\TwigExtraBundle\DependencyInjection\Compiler\TwigCachePoolPass; if (method_exists(KernelInterface::class, 'getShareDir')) { class TwigExtraBundle extends Bundle @@ -24,6 +26,8 @@ if (method_exists(KernelInterface::class, 'getShareDir')) { parent::build($container); $container->addCompilerPass(new MissingExtensionSuggestorPass()); + // priority 64 so that it runs before Symfony's CachePoolPass (priority 32) + $container->addCompilerPass(new TwigCachePoolPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 64); } } } else { @@ -35,6 +39,8 @@ if (method_exists(KernelInterface::class, 'getShareDir')) { parent::build($container); $container->addCompilerPass(new MissingExtensionSuggestorPass()); + // priority 64 so that it runs before Symfony's CachePoolPass (priority 32) + $container->addCompilerPass(new TwigCachePoolPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 64); } } }