From e1c81232f37762d203c52095a965e402ff7e2452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Tue, 21 Apr 2026 13:42:48 +0200 Subject: [PATCH 1/7] refactor entry injection due defered rendering --- phpstan-baseline.neon | 10 ++- src/Asset/FrontendAsset.php | 66 +++++++++++--- .../InjectPageEntriesListener.php | 89 +++++++++++++++---- src/Request/ResponseContext/Entry.php | 13 +++ src/Request/ResponseContext/EntryBag.php | 28 ++++++ 5 files changed, 172 insertions(+), 34 deletions(-) create mode 100644 src/Request/ResponseContext/Entry.php create mode 100644 src/Request/ResponseContext/EntryBag.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3f32295..d5fcc66 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3,13 +3,19 @@ parameters: - message: '#^Call to method getLayout\(\) on an unknown class Contao\\CoreBundle\\Event\\LayoutEvent\.$#' identifier: class.notFound - count: 2 + count: 3 path: src/EventListener/InjectPageEntriesListener.php - message: '#^Call to method getPage\(\) on an unknown class Contao\\CoreBundle\\Event\\LayoutEvent\.$#' identifier: class.notFound - count: 2 + count: 3 + path: src/EventListener/InjectPageEntriesListener.php + + - + message: '#^Call to method getTemplate\(\) on an unknown class Contao\\CoreBundle\\Event\\LayoutEvent\.$#' + identifier: class.notFound + count: 3 path: src/EventListener/InjectPageEntriesListener.php - diff --git a/src/Asset/FrontendAsset.php b/src/Asset/FrontendAsset.php index 31397b4..2f3edb0 100644 --- a/src/Asset/FrontendAsset.php +++ b/src/Asset/FrontendAsset.php @@ -8,38 +8,78 @@ namespace HeimrichHannot\EncoreBundle\Asset; +use Contao\CoreBundle\Routing\ResponseContext\ResponseContextAccessor; +use HeimrichHannot\EncoreBundle\Request\ResponseContext\Entry; +use HeimrichHannot\EncoreBundle\Request\ResponseContext\EntryBag; + class FrontendAsset { - /** - * @var array - */ - private $activeEntrypoints = []; + public function __construct( + private readonly ResponseContextAccessor $responseContextAccessor, + ) { + } /** * Add an active entrypoint. */ - public function addActiveEntrypoint(string $entrypoint): void + public function addActiveEntrypoint(string|Entry $entrypoint): void { - $this->activeEntrypoints[] = $entrypoint; + $bag = $this->getBag(); + if (!$bag) { + return; + } + + if (is_string($entrypoint)) { + $entrypoint = new Entry($entrypoint); + } + + $bag->addEntry($entrypoint); } /** * Return a list of all active entrypoints. * - * @return array + * @return string[] */ - public function getActiveEntrypoints() + public function getActiveEntrypoints(): array { - return $this->activeEntrypoints; + $bag = $this->getBag(); + if (!$bag) { + return []; + } + + return array_map( + static fn (Entry $entry) => $entry->name, + $bag->all() + ); } /** * Check if an entrypoint is set as active entrypoint. - * - * @return bool */ - public function isActiveEntrypoint(string $entrypoint) + public function isActiveEntrypoint(string $entrypoint): bool { - return \in_array($entrypoint, $this->activeEntrypoints, true); + $bag = $this->getBag(); + if (!$bag) { + return false; + } + + return null !== $bag->getEntry($entrypoint); + } + + private function getBag(): ?EntryBag + { + $context = $this->responseContextAccessor->getResponseContext(); + if (!$context) { + return null; + } + if (!$context->has(EntryBag::class)) { + $context->add(new EntryBag()); + } + + /** @var EntryBag $bag */ + $bag = $context->get(EntryBag::class); + + return $bag; } } diff --git a/src/EventListener/InjectPageEntriesListener.php b/src/EventListener/InjectPageEntriesListener.php index d6d6bb7..61728ec 100644 --- a/src/EventListener/InjectPageEntriesListener.php +++ b/src/EventListener/InjectPageEntriesListener.php @@ -26,35 +26,86 @@ public function __construct( #[AsEventListener] public function onLayoutEvent(LayoutEvent $event): void { + if ('regular' !== $event->getPage()->type) { + return; + } + + if ($event->getLayout()->customOption) { + $event->getTemplate()->set('customAttribute', 'Lorem Ipsum'); + } + if (!$this->configurationHelper->isEnabledOnPage($event->getPage(), $event->getLayout())) { return; } - $this->globalContaoAsset->cleanGlobalArrayFromConfiguration(); + $loader = function () use ($event): string { + $this->globalContaoAsset->cleanGlobalArrayFromConfiguration(); + + $entryPoints = $this->entrypointBuilderFactory->create() + ->setPage($event->getPage()) + ->setLayout($event->getLayout()) + ->setFrontendAsset($this->frontendAsset) + ->build(); + + if ($request = $this->requestStack->getCurrentRequest()) { + $request->attributes->add([ + 'encore_entries' => $entryPoints, + ]); + } + + $this->tagRenderer->reset(); + + foreach ($entryPoints->allActive() as $entrypoint) { + if ($entrypoint->requiresCss) { + $GLOBALS['TL_HEAD'][] = $this->tagRenderer->renderWebpackLinkTags($entrypoint->name); + } + if ($entrypoint->head) { + $GLOBALS['TL_HEAD'][] = $this->tagRenderer->renderWebpackScriptTags($entrypoint->name); + } else { + $GLOBALS['TL_BODY'][] = $this->tagRenderer->renderWebpackScriptTags($entrypoint->name); + } + } + + return ''; + }; - $entryPoints = $this->entrypointBuilderFactory->create() - ->setPage($event->getPage()) - ->setLayout($event->getLayout()) - ->setFrontendAsset($this->frontendAsset) - ->build(); + $responseContext = $event->getTemplate()->get('response_context'); + if (!is_object($responseContext)) { + $loader(); - if ($request = $this->requestStack->getCurrentRequest()) { - $request->attributes->add([ - 'encore_entries' => $entryPoints, - ]); + return; } - $this->tagRenderer->reset(); + $responseContext = new class($responseContext, $loader) { + public function __construct( + private $responseContext, + private readonly \Closure $loader, + ) { + } + + public function __get(string $key): mixed + { + if ('end_of_head' === $key) { + ($this->loader)(); + } - foreach ($entryPoints->allActive() as $entrypoint) { - if ($entrypoint->requiresCss) { - $GLOBALS['TL_HEAD'][] = $this->tagRenderer->renderWebpackLinkTags($entrypoint->name); + return $this->responseContext->{$key}; } - if ($entrypoint->head) { - $GLOBALS['TL_HEAD'][] = $this->tagRenderer->renderWebpackScriptTags($entrypoint->name); - } else { - $GLOBALS['TL_BODY'][] = $this->tagRenderer->renderWebpackScriptTags($entrypoint->name); + + public function __isset(string $key): bool + { + if ('end_of_head' === $key) { + return true; + } + + return isset($this->responseContext->{$key}); } - } + + public function __call(string $name, array $arguments): mixed + { + return $this->responseContext->{$name}(...$arguments); + } + }; + $event->getTemplate()->set('response_context', $responseContext); } } diff --git a/src/Request/ResponseContext/Entry.php b/src/Request/ResponseContext/Entry.php new file mode 100644 index 0000000..8fd7153 --- /dev/null +++ b/src/Request/ResponseContext/Entry.php @@ -0,0 +1,13 @@ +entries[$entry->name] = $entry; + + return $this; + } + + public function getEntry(string $name): ?Entry + { + return $this->entries[$name] ?? null; + } + + public function all(): array + { + return $this->entries; + } +} From ea8d9220e1212911f313510f08d64e7de7344660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Tue, 21 Apr 2026 14:29:48 +0200 Subject: [PATCH 2/7] fix tests --- tests/Asset/FrontendAssetTest.php | 44 +++- tests/Asset/PageEntrypointsTest.php | 21 +- tests/EntryPoint/EntryPointsBuilderTest.php | 17 +- .../InjectPageEntriesListenerTest.php | 197 ++++++++++++++++++ 4 files changed, 271 insertions(+), 8 deletions(-) create mode 100644 tests/EventListener/InjectPageEntriesListenerTest.php diff --git a/tests/Asset/FrontendAssetTest.php b/tests/Asset/FrontendAssetTest.php index b72cc01..02ca8e5 100644 --- a/tests/Asset/FrontendAssetTest.php +++ b/tests/Asset/FrontendAssetTest.php @@ -8,18 +8,54 @@ namespace HeimrichHannot\EncoreBundle\Test\Asset; +use Contao\CoreBundle\Routing\ResponseContext\ResponseContext; +use Contao\CoreBundle\Routing\ResponseContext\ResponseContextAccessor; use Contao\TestCase\ContaoTestCase; use HeimrichHannot\EncoreBundle\Asset\FrontendAsset; +use HeimrichHannot\EncoreBundle\Request\ResponseContext\Entry; +use HeimrichHannot\EncoreBundle\Request\ResponseContext\EntryBag; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; class FrontendAssetTest extends ContaoTestCase { - public function testEntrypoints() + public function testEntrypoints(): void { - $frontendAsset = new FrontendAsset(); + $requestStack = new RequestStack(); + $requestStack->push(new Request()); + $responseContextAccessor = new ResponseContextAccessor($requestStack); + $responseContext = new ResponseContext(); + $responseContextAccessor->setResponseContext($responseContext); + $frontendAsset = new FrontendAsset($responseContextAccessor); $frontendAsset->addActiveEntrypoint('contao-encore-bundle'); + $frontendAsset->addActiveEntrypoint(new Entry('contao-slick-bundle', 'origin', 'Extension')); + $this->assertTrue($frontendAsset->isActiveEntrypoint('contao-encore-bundle')); - $this->assertFalse($frontendAsset->isActiveEntrypoint('contao-slick-bundle')); - $this->assertCount(1, $frontendAsset->getActiveEntrypoints()); + $this->assertTrue($frontendAsset->isActiveEntrypoint('contao-slick-bundle')); + $this->assertFalse($frontendAsset->isActiveEntrypoint('contao-missing-bundle')); + $this->assertSame([ + 'contao-encore-bundle' => 'contao-encore-bundle', + 'contao-slick-bundle' => 'contao-slick-bundle', + ], $frontendAsset->getActiveEntrypoints()); + + $this->assertTrue($responseContext->has(EntryBag::class)); + + /** @var EntryBag $bag */ + $bag = $responseContext->get(EntryBag::class); + $this->assertSame('origin', $bag->getEntry('contao-slick-bundle')?->origin); + $this->assertSame('Extension', $bag->getEntry('contao-slick-bundle')?->extension); + } + + public function testEntrypointsWithoutResponseContext(): void + { + $requestStack = new RequestStack(); + $requestStack->push(new Request()); + + $frontendAsset = new FrontendAsset(new ResponseContextAccessor($requestStack)); + $frontendAsset->addActiveEntrypoint('contao-encore-bundle'); + + $this->assertFalse($frontendAsset->isActiveEntrypoint('contao-encore-bundle')); + $this->assertSame([], $frontendAsset->getActiveEntrypoints()); } } diff --git a/tests/Asset/PageEntrypointsTest.php b/tests/Asset/PageEntrypointsTest.php index 8dc6d19..8c96132 100644 --- a/tests/Asset/PageEntrypointsTest.php +++ b/tests/Asset/PageEntrypointsTest.php @@ -8,6 +8,8 @@ namespace HeimrichHannot\EncoreBundle\Test\Asset; +use Contao\CoreBundle\Routing\ResponseContext\ResponseContext; +use Contao\CoreBundle\Routing\ResponseContext\ResponseContextAccessor; use Contao\LayoutModel; use Contao\PageModel; use Contao\TestCase\ContaoTestCase; @@ -19,6 +21,8 @@ use HeimrichHannot\UtilsBundle\Util\Utils; use PHPUnit\Framework\Error\Warning; use PHPUnit\Framework\MockObject\MockObject; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; class PageEntrypointsTest extends ContaoTestCase { @@ -73,6 +77,17 @@ function (&$item, $key) { return new PageEntrypoints($frontendAsset, $entryCollection, $utils); } + private function createFrontendAsset(): FrontendAsset + { + $requestStack = new RequestStack(); + $requestStack->push(new Request()); + + $responseContextAccessor = new ResponseContextAccessor($requestStack); + $responseContextAccessor->setResponseContext(new ResponseContext()); + + return new FrontendAsset($responseContextAccessor); + } + public function entryPointProvider() { return [ @@ -270,7 +285,7 @@ function (&$item, $key) { array_merge(($bundleConfig['js_entries'] ?? []), ($bundleConfig['entrypoints_jsons'] ?? [])) ); - $frontendAsset = new FrontendAsset(); + $frontendAsset = $this->createFrontendAsset(); $frontendAsset->addActiveEntrypoint('contao-slick-bundle'); $pageEntrypoints = $this->createTestInstance([ @@ -376,7 +391,7 @@ public function testPageEntryOrder($pageParents, $bundleConfig, $page, $layout, $entryCollection = $this->createMock(EntryCollection::class); $entryCollection->method('getEntries')->willReturn(($bundleConfig['js_entries'] ?? [])); - $frontendAsset = new FrontendAsset(); + $frontendAsset = $this->createFrontendAsset(); $frontendAsset->addActiveEntrypoint('contao-slick-bundle'); $pageEntrypoints = $this->createTestInstance([ @@ -420,7 +435,7 @@ public function testGetter() ], ]; - $frontendAsset = new FrontendAsset(); + $frontendAsset = $this->createFrontendAsset(); $frontendAsset->addActiveEntrypoint('contao-slick-bundle'); $pageEntrypoints = $this->createTestInstance([ diff --git a/tests/EntryPoint/EntryPointsBuilderTest.php b/tests/EntryPoint/EntryPointsBuilderTest.php index 2598343..da00a54 100644 --- a/tests/EntryPoint/EntryPointsBuilderTest.php +++ b/tests/EntryPoint/EntryPointsBuilderTest.php @@ -2,6 +2,8 @@ namespace HeimrichHannot\EncoreBundle\Test\EntryPoint; +use Contao\CoreBundle\Routing\ResponseContext\ResponseContext; +use Contao\CoreBundle\Routing\ResponseContext\ResponseContextAccessor; use Contao\LayoutModel; use Contao\PageModel; use Contao\TestCase\ContaoTestCase; @@ -14,11 +16,24 @@ use HeimrichHannot\TestUtilitiesBundle\Mock\ModelMockTrait; use HeimrichHannot\UtilsBundle\Util\ModelUtil; use HeimrichHannot\UtilsBundle\Util\Utils; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; class EntryPointsBuilderTest extends ContaoTestCase { use ModelMockTrait; + private function createFrontendAsset(): FrontendAsset + { + $requestStack = new RequestStack(); + $requestStack->push(new Request()); + + $responseContextAccessor = new ResponseContextAccessor($requestStack); + $responseContextAccessor->setResponseContext(new ResponseContext()); + + return new FrontendAsset($responseContextAccessor); + } + public function testFactoryCreatesFreshBuilderInstances(): void { $utils = $this->createMock(Utils::class); @@ -118,7 +133,7 @@ public function testBuildCombinesFrontendLayoutAndPageEntries(): void ->method('model') ->willReturn($modelUtil); - $frontendAsset = new FrontendAsset(); + $frontendAsset = $this->createFrontendAsset(); $frontendAsset->addActiveEntrypoint('frontend-entry'); $frontendAsset->addActiveEntrypoint('missing-frontend-entry'); diff --git a/tests/EventListener/InjectPageEntriesListenerTest.php b/tests/EventListener/InjectPageEntriesListenerTest.php new file mode 100644 index 0000000..ea13609 --- /dev/null +++ b/tests/EventListener/InjectPageEntriesListenerTest.php @@ -0,0 +1,197 @@ +data[$key] = $value; + } + + public function get(string $key): mixed + { + return $this->data[$key] ?? null; + } + + public function has(string $key): bool + { + return \array_key_exists($key, $this->data); + } + } + + class_alias(InjectPageEntriesTestLayoutTemplate::class, \Contao\CoreBundle\Twig\LayoutTemplate::class); +} + +if (!class_exists(\Contao\CoreBundle\Event\LayoutEvent::class)) { + class InjectPageEntriesTestLayoutEvent + { + public function __construct( + private readonly object $template, + private readonly PageModel $page, + private readonly LayoutModel $layout, + ) { + } + + public function getTemplate(): object + { + return $this->template; + } + + public function getPage(): PageModel + { + return $this->page; + } + + public function getLayout(): LayoutModel + { + return $this->layout; + } + } + + class_alias(InjectPageEntriesTestLayoutEvent::class, \Contao\CoreBundle\Event\LayoutEvent::class); +} + +class InjectPageEntriesListenerTest extends ContaoTestCase +{ + use ModelMockTrait; + + private function createTestInstance(array $parameters = []): InjectPageEntriesListener + { + return new InjectPageEntriesListener( + $parameters['tagRenderer'] ?? $this->createMock(TagRenderer::class), + $parameters['entrypointBuilderFactory'] ?? $this->createMock(EntryPointBuilderFactory::class), + $parameters['frontendAsset'] ?? $this->createMock(FrontendAsset::class), + $parameters['globalContaoAsset'] ?? $this->createMock(GlobalContaoAsset::class), + $parameters['configurationHelper'] ?? $this->createMock(ConfigurationHelper::class), + $parameters['requestStack'] ?? $this->createMock(RequestStack::class), + ); + } + + public function testOnLayoutEventLoadsEntrypointsViaLazyResponseContext(): void + { + $GLOBALS['TL_HEAD'] = []; + $GLOBALS['TL_BODY'] = []; + + $page = $this->mockModelObject(PageModel::class, ['type' => 'regular']); + $layout = $this->mockClassWithProperties(LayoutModel::class, ['customOption' => true]); + + $configurationHelper = $this->createMock(ConfigurationHelper::class); + $configurationHelper->expects($this->once()) + ->method('isEnabledOnPage') + ->with($page, $layout) + ->willReturn(true); + + $entryPoints = new EntryPoints(); + $entryPoints->add(new EntryPoint('app', head: true, requiresCss: true)); + $entryPoints->add(new EntryPoint('deferred', head: false, requiresCss: false)); + + $builder = $this->createMock(EntryPointsBuilder::class); + $builder->expects($this->once())->method('setPage')->with($page)->willReturnSelf(); + $builder->expects($this->once())->method('setLayout')->with($layout)->willReturnSelf(); + $builder->expects($this->once())->method('setFrontendAsset')->with($this->isInstanceOf(FrontendAsset::class))->willReturnSelf(); + $builder->expects($this->once())->method('build')->willReturn($entryPoints); + + $entryPointBuilderFactory = $this->createMock(EntryPointBuilderFactory::class); + $entryPointBuilderFactory->expects($this->once())->method('create')->willReturn($builder); + + $globalContaoAsset = $this->createMock(GlobalContaoAsset::class); + $globalContaoAsset->expects($this->once())->method('cleanGlobalArrayFromConfiguration'); + + $tagRenderer = $this->createMock(TagRenderer::class); + $tagRenderer->expects($this->once())->method('reset'); + $tagRenderer->expects($this->once())->method('renderWebpackLinkTags')->with('app')->willReturn(''); + $tagRenderer->expects($this->exactly(2)) + ->method('renderWebpackScriptTags') + ->willReturnCallback(static fn (string $entryName): string => match ($entryName) { + 'app' => '', + 'deferred' => '', + default => throw new \InvalidArgumentException(sprintf('Unexpected entry "%s".', $entryName)), + }); + + $request = new Request(); + $requestStack = $this->createMock(RequestStack::class); + $requestStack->expects($this->once())->method('getCurrentRequest')->willReturn($request); + + $template = new LayoutTemplate('layout', static fn () => new Response()); + $responseContext = new class() { + public string $end_of_head = 'existing-head'; + public string $other = 'other-value'; + + public function ping(string $value): string + { + return 'pong-'.$value; + } + }; + $template->set('response_context', $responseContext); + + $listener = $this->createTestInstance([ + 'tagRenderer' => $tagRenderer, + 'entrypointBuilderFactory' => $entryPointBuilderFactory, + 'globalContaoAsset' => $globalContaoAsset, + 'configurationHelper' => $configurationHelper, + 'requestStack' => $requestStack, + ]); + + $listener->onLayoutEvent(new LayoutEvent($template, $page, $layout)); + + $this->assertSame('Lorem Ipsum', $template->get('customAttribute')); + + $wrappedResponseContext = $template->get('response_context'); + $this->assertSame('other-value', $wrappedResponseContext->other); + $this->assertTrue(isset($wrappedResponseContext->end_of_head)); + $this->assertSame('pong-demo', $wrappedResponseContext->ping('demo')); + $this->assertSame([], $GLOBALS['TL_HEAD']); + $this->assertSame([], $GLOBALS['TL_BODY']); + + $this->assertSame('existing-head', $wrappedResponseContext->end_of_head); + $this->assertSame($entryPoints, $request->attributes->get('encore_entries')); + $this->assertSame(['', ''], $GLOBALS['TL_HEAD']); + $this->assertSame([''], $GLOBALS['TL_BODY']); + } + + public function testOnLayoutEventReturnsEarlyForNonRegularPages(): void + { + $page = $this->mockModelObject(PageModel::class, ['type' => 'error_404']); + $layout = $this->mockClassWithProperties(LayoutModel::class, ['customOption' => false]); + + $configurationHelper = $this->createMock(ConfigurationHelper::class); + $configurationHelper->expects($this->never())->method('isEnabledOnPage'); + + $template = new LayoutTemplate('layout', static fn () => new Response()); + + $listener = $this->createTestInstance([ + 'configurationHelper' => $configurationHelper, + ]); + + $listener->onLayoutEvent(new LayoutEvent($template, $page, $layout)); + + $this->assertFalse($template->has('customAttribute')); + } +} From cbc1d2b35ec54dd5a7a047b9fa48554e9e414ddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Tue, 21 Apr 2026 15:33:58 +0200 Subject: [PATCH 3/7] use EntryBag everywhere --- .../twig/data_collector/huh_encore.html.twig | 12 +++--- docs/developers.md | 14 ++++--- docs/developers/dynamic_entries.md | 28 +++++++++++++- src/Asset/FrontendAsset.php | 2 +- src/EntryPoint/EntryPoint.php | 11 ++++++ src/EntryPoint/EntryPointsBuilder.php | 38 +++++++++++++------ .../ReplaceDynamicScriptTagsListener.php | 6 +-- .../InjectPageEntriesListener.php | 6 +-- 8 files changed, 86 insertions(+), 31 deletions(-) diff --git a/contao/templates/twig/data_collector/huh_encore.html.twig b/contao/templates/twig/data_collector/huh_encore.html.twig index 6798126..5952ccd 100644 --- a/contao/templates/twig/data_collector/huh_encore.html.twig +++ b/contao/templates/twig/data_collector/huh_encore.html.twig @@ -95,12 +95,12 @@ {% for entry in collector.entries %} - {{ entry.name }} - {{ entry.active }} - {{ entry.head }} - {{ entry.requiresCss }} - {{ entry.origin }} - {{ entry.extension }} + {{ entry.name }} + {{ entry.active ? 'yes' : '' }} + {{ entry.head ? 'yes' : '' }} + {{ entry.requiresCss ? 'yes' : '' }} + {{ entry.origin }} + {{ entry.extension }} {% endfor %} diff --git a/docs/developers.md b/docs/developers.md index 6849788..46cf746 100644 --- a/docs/developers.md +++ b/docs/developers.md @@ -69,27 +69,29 @@ To collect or render assets in custom templates or abstinent from the normal pag namespace App\CustomController; -use HeimrichHannot\EncoreBundle\Asset\FrontendAsset; use HeimrichHannot\EncoreBundle\EntryPoint\EntryPointBuilderFactory; -use Symfony\Component\EventDispatcher\Attribute\AsEventListener; +use HeimrichHannot\EncoreBundle\Request\ResponseContext\Entry; +use HeimrichHannot\EncoreBundle\Request\ResponseContext\EntryBag; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\WebpackEncoreBundle\Asset\TagRenderer;use Twig\Environment; +use Symfony\WebpackEncoreBundle\Asset\TagRenderer; +use Twig\Environment; class CustomController { private readonly TagRenderer $tagRenderer; private readonly EntryPointBuilderFactory $entrypointBuilderFactory; private readonly Environment $twig; - private readonly FrontendAsset $frontendAsset; - public function __invoke(): Response + public function __invoke(Request $request): Response { // collect entry points from the different sources $entryPoints = $this->entrypointBuilderFactory->create() // add the sources you want: ->setPage($event->getPage()) ->setLayout($event->getLayout()) - ->setFrontendAsset($this->frontendAsset) + ->setResponseContext($request->attributes->get(ResponseContext::REQUEST_ATTRIBUTE_NAME)) + ->setCustomBag((new EntryBag())->addEntry(new Entry('additional_entry', __METHOD__, 'App'))) // build the collection: ->build(); diff --git a/docs/developers/dynamic_entries.md b/docs/developers/dynamic_entries.md index 7c90e9c..1e3e3d4 100644 --- a/docs/developers/dynamic_entries.md +++ b/docs/developers/dynamic_entries.md @@ -4,7 +4,33 @@ This document describes different ways to add entries from your code. > For most usecases, you should use the [PageAssetTrait](../developers.md#add-encore-entries-to-custom-template) instead! -### FrontendAsset service + +## Response context + +> Since version 2.2 + +Use `EntryBag` of `ResponseContext` to add entries from your controller. + +```php +class CustomController +{ + public function __invoke(Request $request): Response + { + $responseContext = $request->attributes->get(ResponseContext::REQUEST_ATTRIBUTE_NAME); + if ($responseContext instanceof ResponseContext) { + if (!$responseContext->has(EntryBag::class)) { + $responseContext->add(new EntryBag()); + } + $responseContext->get(EntryBag::class) + ?->addEntry(new Entry('contao-tagsinput', __METHOD__, 'example-vendor/example-extension')); + } + } +} +``` + +Read more about the `ResponseContext` in the [contao docs](https://docs.contao.org/5.x/dev/framework/response-context/). + +## FrontendAsset service Encore bundle comes with a service, `FrontendAsset`, to register your entrypoints. diff --git a/src/Asset/FrontendAsset.php b/src/Asset/FrontendAsset.php index 2f3edb0..9a9ff3c 100644 --- a/src/Asset/FrontendAsset.php +++ b/src/Asset/FrontendAsset.php @@ -30,7 +30,7 @@ public function addActiveEntrypoint(string|Entry $entrypoint): void } if (is_string($entrypoint)) { - $entrypoint = new Entry($entrypoint); + $entrypoint = new Entry($entrypoint, __METHOD__); } $bag->addEntry($entrypoint); diff --git a/src/EntryPoint/EntryPoint.php b/src/EntryPoint/EntryPoint.php index e49910b..dcf4a2c 100644 --- a/src/EntryPoint/EntryPoint.php +++ b/src/EntryPoint/EntryPoint.php @@ -2,6 +2,8 @@ namespace HeimrichHannot\EncoreBundle\EntryPoint; +use HeimrichHannot\EncoreBundle\Request\ResponseContext\Entry; + class EntryPoint { public function __construct( @@ -13,4 +15,13 @@ public function __construct( public readonly string $extension = '', ) { } + + public static function fromEntry(Entry $entry): self + { + return new self( + name: $entry->name, + origin: $entry->origin, + extension: $entry->extension, + ); + } } diff --git a/src/EntryPoint/EntryPointsBuilder.php b/src/EntryPoint/EntryPointsBuilder.php index dd83180..4a4f13e 100644 --- a/src/EntryPoint/EntryPointsBuilder.php +++ b/src/EntryPoint/EntryPointsBuilder.php @@ -2,12 +2,13 @@ namespace HeimrichHannot\EncoreBundle\EntryPoint; +use Contao\CoreBundle\Routing\ResponseContext\ResponseContext; use Contao\LayoutModel; use Contao\PageModel; use Contao\StringUtil; -use HeimrichHannot\EncoreBundle\Asset\FrontendAsset; use HeimrichHannot\EncoreBundle\Collection\EntryCollection; use HeimrichHannot\EncoreBundle\Dca\EncoreEntriesSelectField; +use HeimrichHannot\EncoreBundle\Request\ResponseContext\EntryBag; use HeimrichHannot\UtilsBundle\Util\Utils; class EntryPointsBuilder @@ -16,9 +17,10 @@ class EntryPointsBuilder private string $pageField = ''; private ?LayoutModel $layout = null; private string $layoutField = ''; - private ?FrontendAsset $frontendAsset = null; private array $available = []; + private ?ResponseContext $responseContext = null; + private ?EntryBag $entryBag; public function __construct( private readonly Utils $utils, @@ -42,13 +44,19 @@ public function setLayout(?LayoutModel $layout, string $field = EncoreEntriesSel return $this; } - public function setFrontendAsset(?FrontendAsset $frontendAsset): self + public function setResponseContext(?ResponseContext $responseContext): self { - $this->frontendAsset = $frontendAsset; + $this->responseContext = $responseContext; return $this; } + public function setCustomBag(?EntryBag $entryBag): self + { + $this->entryBag = $entryBag; + return $this; + } + public function build(): EntryPoints { $entryPoints = new EntryPoints(); @@ -58,13 +66,10 @@ public function build(): EntryPoints } $this->available = $available; - if ($this->frontendAsset) { - foreach ($this->frontendAsset->getActiveEntrypoints() as $entryPoint) { - $this->addEntryPoint( - entryPoints: $entryPoints, - name: $entryPoint, - origin: FrontendAsset::class, - ); + if ($this->responseContext) { + $bag = $this->responseContext->get(EntryBag::class); + if ($bag instanceof EntryBag) { + $this->addFromBag($entryPoints, $bag); } } @@ -105,9 +110,20 @@ public function build(): EntryPoints } } + if (null !== $this->entryBag) { + $this->addFromBag($entryPoints, $this->entryBag); + } + return $entryPoints; } + private function addFromBag(EntryPoints $entryPoints, EntryBag $bag): void + { + foreach ($bag->all() as $entry) { + $entryPoints->add(EntryPoint::fromEntry($entry)); + } + } + private function addEntryPoint(EntryPoints $entryPoints, string $name, bool $active = true, string $origin = '', string $extension = ''): void { if ('' === $name) { diff --git a/src/EventListener/Contao/ReplaceDynamicScriptTagsListener.php b/src/EventListener/Contao/ReplaceDynamicScriptTagsListener.php index eaa69bc..c1170e6 100644 --- a/src/EventListener/Contao/ReplaceDynamicScriptTagsListener.php +++ b/src/EventListener/Contao/ReplaceDynamicScriptTagsListener.php @@ -10,7 +10,7 @@ use Contao\CoreBundle\DependencyInjection\Attribute\AsHook; use Contao\CoreBundle\Framework\ContaoFramework; -use HeimrichHannot\EncoreBundle\Asset\FrontendAsset; +use Contao\CoreBundle\Routing\ResponseContext\ResponseContextAccessor; use HeimrichHannot\EncoreBundle\Asset\GlobalContaoAsset; use HeimrichHannot\EncoreBundle\EntryPoint\EntryPointBuilderFactory; use HeimrichHannot\EncoreBundle\Helper\ConfigurationHelper; @@ -26,9 +26,9 @@ public function __construct( protected ConfigurationHelper $configurationHelper, private readonly GlobalContaoAsset $globalContaoAsset, private readonly EntryPointBuilderFactory $entryPointBuilderFactory, - private readonly FrontendAsset $frontendAsset, private readonly TagRenderer $tagRenderer, private readonly RequestStack $requestStack, + private readonly ResponseContextAccessor $responseContextAccessor, ) { } @@ -45,7 +45,7 @@ public function __invoke(string $buffer): string } $entryPoints = $this->entryPointBuilderFactory->create() - ->setFrontendAsset($this->frontendAsset) + ->setResponseContext($this->responseContextAccessor->getResponseContext()) ->setPage($pageModel) ->build(); diff --git a/src/EventListener/InjectPageEntriesListener.php b/src/EventListener/InjectPageEntriesListener.php index 61728ec..981ba97 100644 --- a/src/EventListener/InjectPageEntriesListener.php +++ b/src/EventListener/InjectPageEntriesListener.php @@ -3,7 +3,7 @@ namespace HeimrichHannot\EncoreBundle\EventListener; use Contao\CoreBundle\Event\LayoutEvent; -use HeimrichHannot\EncoreBundle\Asset\FrontendAsset; +use Contao\CoreBundle\Routing\ResponseContext\ResponseContextAccessor; use HeimrichHannot\EncoreBundle\Asset\GlobalContaoAsset; use HeimrichHannot\EncoreBundle\EntryPoint\EntryPointBuilderFactory; use HeimrichHannot\EncoreBundle\Helper\ConfigurationHelper; @@ -16,10 +16,10 @@ class InjectPageEntriesListener public function __construct( private readonly TagRenderer $tagRenderer, private readonly EntryPointBuilderFactory $entrypointBuilderFactory, - private readonly FrontendAsset $frontendAsset, private readonly GlobalContaoAsset $globalContaoAsset, private readonly ConfigurationHelper $configurationHelper, private readonly RequestStack $requestStack, + private readonly ResponseContextAccessor $responseContextAccessor, ) { } @@ -44,7 +44,7 @@ public function onLayoutEvent(LayoutEvent $event): void $entryPoints = $this->entrypointBuilderFactory->create() ->setPage($event->getPage()) ->setLayout($event->getLayout()) - ->setFrontendAsset($this->frontendAsset) + ->setResponseContext($this->responseContextAccessor->getResponseContext()) ->build(); if ($request = $this->requestStack->getCurrentRequest()) { From 542968519560531e0661dc6f56a3430025ceeeae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Tue, 21 Apr 2026 16:02:08 +0200 Subject: [PATCH 4/7] adept tests --- tests/EntryPoint/EntryPointsBuilderTest.php | 35 ++++++++----------- .../ReplaceDynamicScriptTagsListenerTest.php | 32 +++++++++++++---- .../InjectPageEntriesListenerTest.php | 24 +++++++++++-- 3 files changed, 60 insertions(+), 31 deletions(-) diff --git a/tests/EntryPoint/EntryPointsBuilderTest.php b/tests/EntryPoint/EntryPointsBuilderTest.php index da00a54..6e49531 100644 --- a/tests/EntryPoint/EntryPointsBuilderTest.php +++ b/tests/EntryPoint/EntryPointsBuilderTest.php @@ -7,33 +7,21 @@ use Contao\LayoutModel; use Contao\PageModel; use Contao\TestCase\ContaoTestCase; -use HeimrichHannot\EncoreBundle\Asset\FrontendAsset; use HeimrichHannot\EncoreBundle\Collection\EntryCollection; use HeimrichHannot\EncoreBundle\EntryPoint\EntryPoint; use HeimrichHannot\EncoreBundle\EntryPoint\EntryPointBuilderFactory; use HeimrichHannot\EncoreBundle\EntryPoint\EntryPoints; use HeimrichHannot\EncoreBundle\EntryPoint\EntryPointsBuilder; +use HeimrichHannot\EncoreBundle\Request\ResponseContext\Entry; +use HeimrichHannot\EncoreBundle\Request\ResponseContext\EntryBag; use HeimrichHannot\TestUtilitiesBundle\Mock\ModelMockTrait; use HeimrichHannot\UtilsBundle\Util\ModelUtil; use HeimrichHannot\UtilsBundle\Util\Utils; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\RequestStack; class EntryPointsBuilderTest extends ContaoTestCase { use ModelMockTrait; - private function createFrontendAsset(): FrontendAsset - { - $requestStack = new RequestStack(); - $requestStack->push(new Request()); - - $responseContextAccessor = new ResponseContextAccessor($requestStack); - $responseContextAccessor->setResponseContext(new ResponseContext()); - - return new FrontendAsset($responseContextAccessor); - } - public function testFactoryCreatesFreshBuilderInstances(): void { $utils = $this->createMock(Utils::class); @@ -133,13 +121,17 @@ public function testBuildCombinesFrontendLayoutAndPageEntries(): void ->method('model') ->willReturn($modelUtil); - $frontendAsset = $this->createFrontendAsset(); - $frontendAsset->addActiveEntrypoint('frontend-entry'); - $frontendAsset->addActiveEntrypoint('missing-frontend-entry'); + $responseContext = new ResponseContext(); + $responseContext->add( + (new EntryBag()) + ->addEntry(new Entry('frontend-entry', 'frontend', 'App')) + ->addEntry(new Entry('missing-frontend-entry', 'frontend', 'App')) + ); $builder = new EntryPointsBuilder($utils, $entryCollection); $result = $builder - ->setFrontendAsset($frontendAsset) + ->setResponseContext($responseContext) + ->setCustomBag(null) ->setLayout($layout, 'layoutEntries') ->setPage($page, 'customEntries') ->build(); @@ -150,16 +142,17 @@ public function testBuildCombinesFrontendLayoutAndPageEntries(): void $active = $result->allActive(); $this->assertSame( - ['frontend-entry', 'layout-entry', 'shared-entry', 'parent-entry', 'page-entry'], + ['frontend-entry', 'missing-frontend-entry', 'layout-entry', 'shared-entry', 'parent-entry', 'page-entry'], array_keys($all) ); $this->assertSame( - ['frontend-entry', 'layout-entry', 'parent-entry', 'page-entry'], + ['frontend-entry', 'missing-frontend-entry', 'layout-entry', 'parent-entry', 'page-entry'], array_keys($active) ); - $this->assertSame(FrontendAsset::class, $all['frontend-entry']->origin); + $this->assertSame('frontend', $all['frontend-entry']->origin); $this->assertFalse($all['frontend-entry']->requiresCss); + $this->assertSame('App', $all['missing-frontend-entry']->extension); $this->assertTrue($all['layout-entry']->head); $this->assertTrue($all['layout-entry']->requiresCss); $this->assertSame('tl_layout.5', $all['layout-entry']->origin); diff --git a/tests/EventListener/Contao/ReplaceDynamicScriptTagsListenerTest.php b/tests/EventListener/Contao/ReplaceDynamicScriptTagsListenerTest.php index 725643b..ca8a0ed 100644 --- a/tests/EventListener/Contao/ReplaceDynamicScriptTagsListenerTest.php +++ b/tests/EventListener/Contao/ReplaceDynamicScriptTagsListenerTest.php @@ -9,9 +9,10 @@ namespace HeimrichHannot\EncoreBundle\Test\EventListener\Contao; use Contao\CoreBundle\Framework\ContaoFramework; +use Contao\CoreBundle\Routing\ResponseContext\ResponseContext; +use Contao\CoreBundle\Routing\ResponseContext\ResponseContextAccessor; use Contao\PageModel; use Contao\TestCase\ContaoTestCase; -use HeimrichHannot\EncoreBundle\Asset\FrontendAsset; use HeimrichHannot\EncoreBundle\Asset\GlobalContaoAsset; use HeimrichHannot\EncoreBundle\EntryPoint\EntryPoint; use HeimrichHannot\EncoreBundle\EntryPoint\EntryPointBuilderFactory; @@ -30,24 +31,38 @@ class ReplaceDynamicScriptTagsListenerTest extends ContaoTestCase { use ModelMockTrait; + private function createResponseContextAccessor(?ResponseContext $responseContext = null): ResponseContextAccessor + { + $requestStack = new RequestStack(); + $requestStack->push(new Request()); + + $accessor = new ResponseContextAccessor($requestStack); + + if (null !== $responseContext) { + $accessor->setResponseContext($responseContext); + } + + return $accessor; + } + public function createTestInstance(array $parameter = []): ReplaceDynamicScriptTagsListener { $parameter['utils'] = $parameter['utils'] ?? $this->createMock(Utils::class); $parameter['configurationHelper'] = $parameter['configurationHelper'] ?? $this->createMock(ConfigurationHelper::class); $parameter['globalContaoAsset'] = $parameter['globalContaoAsset'] ?? $this->createMock(GlobalContaoAsset::class); $parameter['entryPointBuilderFactory'] = $parameter['entryPointBuilderFactory'] ?? $this->createMock(EntryPointBuilderFactory::class); - $parameter['frontendAsset'] = $parameter['frontendAsset'] ?? $this->createMock(FrontendAsset::class); $parameter['tagRenderer'] = $parameter['tagRenderer'] ?? $this->createMock(TagRenderer::class); $parameter['requestStack'] = $parameter['requestStack'] ?? $this->createMock(RequestStack::class); + $parameter['responseContextAccessor'] = $parameter['responseContextAccessor'] ?? $this->createResponseContextAccessor(); return new ReplaceDynamicScriptTagsListener( $parameter['utils'], $parameter['configurationHelper'], $parameter['globalContaoAsset'], - entryPointBuilderFactory: $parameter['entryPointBuilderFactory'], - frontendAsset: $parameter['frontendAsset'], - tagRenderer: $parameter['tagRenderer'], - requestStack: $parameter['requestStack'] + $parameter['entryPointBuilderFactory'], + $parameter['tagRenderer'], + $parameter['requestStack'], + $parameter['responseContextAccessor'], ); } @@ -113,8 +128,10 @@ public function testInvoke() $entryPoints->add(new EntryPoint('deferred', head: false, requiresCss: false)); $entryPoints->add(new EntryPoint('inactive', active: false, head: true, requiresCss: true)); + $responseContext = new ResponseContext(); + $builder = $this->createMock(EntryPointsBuilder::class); - $builder->expects($this->once())->method('setFrontendAsset')->with($this->isInstanceOf(FrontendAsset::class))->willReturnSelf(); + $builder->expects($this->once())->method('setResponseContext')->with($responseContext)->willReturnSelf(); $builder->expects($this->once())->method('setPage')->with($pageModel)->willReturnSelf(); $builder->expects($this->once())->method('build')->willReturn($entryPoints); @@ -156,6 +173,7 @@ public function testInvoke() 'entryPointBuilderFactory' => $entryPointBuilderFactory, 'tagRenderer' => $tagRenderer, 'requestStack' => $requestStack, + 'responseContextAccessor' => $this->createResponseContextAccessor($responseContext), ]); $nonce = '_' . ContaoFramework::getNonce(); diff --git a/tests/EventListener/InjectPageEntriesListenerTest.php b/tests/EventListener/InjectPageEntriesListenerTest.php index ea13609..869c732 100644 --- a/tests/EventListener/InjectPageEntriesListenerTest.php +++ b/tests/EventListener/InjectPageEntriesListenerTest.php @@ -3,11 +3,12 @@ namespace HeimrichHannot\EncoreBundle\Test\EventListener; use Contao\CoreBundle\Event\LayoutEvent; +use Contao\CoreBundle\Routing\ResponseContext\ResponseContext; +use Contao\CoreBundle\Routing\ResponseContext\ResponseContextAccessor; use Contao\CoreBundle\Twig\LayoutTemplate; use Contao\LayoutModel; use Contao\PageModel; use Contao\TestCase\ContaoTestCase; -use HeimrichHannot\EncoreBundle\Asset\FrontendAsset; use HeimrichHannot\EncoreBundle\Asset\GlobalContaoAsset; use HeimrichHannot\EncoreBundle\EntryPoint\EntryPoint; use HeimrichHannot\EncoreBundle\EntryPoint\EntryPointBuilderFactory; @@ -82,15 +83,29 @@ class InjectPageEntriesListenerTest extends ContaoTestCase { use ModelMockTrait; + private function createResponseContextAccessor(?ResponseContext $responseContext = null): ResponseContextAccessor + { + $requestStack = new RequestStack(); + $requestStack->push(new Request()); + + $accessor = new ResponseContextAccessor($requestStack); + + if (null !== $responseContext) { + $accessor->setResponseContext($responseContext); + } + + return $accessor; + } + private function createTestInstance(array $parameters = []): InjectPageEntriesListener { return new InjectPageEntriesListener( $parameters['tagRenderer'] ?? $this->createMock(TagRenderer::class), $parameters['entrypointBuilderFactory'] ?? $this->createMock(EntryPointBuilderFactory::class), - $parameters['frontendAsset'] ?? $this->createMock(FrontendAsset::class), $parameters['globalContaoAsset'] ?? $this->createMock(GlobalContaoAsset::class), $parameters['configurationHelper'] ?? $this->createMock(ConfigurationHelper::class), $parameters['requestStack'] ?? $this->createMock(RequestStack::class), + $parameters['responseContextAccessor'] ?? $this->createResponseContextAccessor(), ); } @@ -112,10 +127,12 @@ public function testOnLayoutEventLoadsEntrypointsViaLazyResponseContext(): void $entryPoints->add(new EntryPoint('app', head: true, requiresCss: true)); $entryPoints->add(new EntryPoint('deferred', head: false, requiresCss: false)); + $responseContextState = new ResponseContext(); + $builder = $this->createMock(EntryPointsBuilder::class); $builder->expects($this->once())->method('setPage')->with($page)->willReturnSelf(); $builder->expects($this->once())->method('setLayout')->with($layout)->willReturnSelf(); - $builder->expects($this->once())->method('setFrontendAsset')->with($this->isInstanceOf(FrontendAsset::class))->willReturnSelf(); + $builder->expects($this->once())->method('setResponseContext')->with($responseContextState)->willReturnSelf(); $builder->expects($this->once())->method('build')->willReturn($entryPoints); $entryPointBuilderFactory = $this->createMock(EntryPointBuilderFactory::class); @@ -157,6 +174,7 @@ public function ping(string $value): string 'globalContaoAsset' => $globalContaoAsset, 'configurationHelper' => $configurationHelper, 'requestStack' => $requestStack, + 'responseContextAccessor' => $this->createResponseContextAccessor($responseContextState), ]); $listener->onLayoutEvent(new LayoutEvent($template, $page, $layout)); From 343302121ab76979879f17a6ba6489314a35b331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Tue, 21 Apr 2026 16:15:05 +0200 Subject: [PATCH 5/7] incorporate review comments --- CHANGELOG.md | 2 ++ src/EntryPoint/EntryPoint.php | 9 --------- src/EntryPoint/EntryPointsBuilder.php | 11 ++++++++--- src/EventListener/InjectPageEntriesListener.php | 8 -------- 4 files changed, 10 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b0033a..265ea12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ All notable changes to this project will be documented in this file. - Deprecated: `ConfigurationHelper::isEnabledOnPage()` ([#34](https://github.com/heimrichhannot/contao-encore-bundle/pull/34)) - Deprecated: getter-Methods in `EncoreEnabledEvent` ([#34](https://github.com/heimrichhannot/contao-encore-bundle/pull/34)) +- Added: ResponseContext bag for entries ([#36](https://github.com/heimrichhannot/contao-encore-bundle/pull/36)) + ## [2.1.1] - 2026-03-30 - Fixed: compatibility issue with symfony 7 diff --git a/src/EntryPoint/EntryPoint.php b/src/EntryPoint/EntryPoint.php index dcf4a2c..fba9078 100644 --- a/src/EntryPoint/EntryPoint.php +++ b/src/EntryPoint/EntryPoint.php @@ -15,13 +15,4 @@ public function __construct( public readonly string $extension = '', ) { } - - public static function fromEntry(Entry $entry): self - { - return new self( - name: $entry->name, - origin: $entry->origin, - extension: $entry->extension, - ); - } } diff --git a/src/EntryPoint/EntryPointsBuilder.php b/src/EntryPoint/EntryPointsBuilder.php index 4a4f13e..e3cb274 100644 --- a/src/EntryPoint/EntryPointsBuilder.php +++ b/src/EntryPoint/EntryPointsBuilder.php @@ -20,7 +20,7 @@ class EntryPointsBuilder private array $available = []; private ?ResponseContext $responseContext = null; - private ?EntryBag $entryBag; + private ?EntryBag $entryBag = null; public function __construct( private readonly Utils $utils, @@ -66,7 +66,7 @@ public function build(): EntryPoints } $this->available = $available; - if ($this->responseContext) { + if ($this->responseContext && $this->responseContext->has(EntryBag::class)) { $bag = $this->responseContext->get(EntryBag::class); if ($bag instanceof EntryBag) { $this->addFromBag($entryPoints, $bag); @@ -120,7 +120,12 @@ public function build(): EntryPoints private function addFromBag(EntryPoints $entryPoints, EntryBag $bag): void { foreach ($bag->all() as $entry) { - $entryPoints->add(EntryPoint::fromEntry($entry)); + $this->addEntryPoint( + entryPoints: $entryPoints, + name: $entry->name, + origin: $entry->origin, + extension: $entry->extension, + ); } } diff --git a/src/EventListener/InjectPageEntriesListener.php b/src/EventListener/InjectPageEntriesListener.php index 981ba97..126f7ab 100644 --- a/src/EventListener/InjectPageEntriesListener.php +++ b/src/EventListener/InjectPageEntriesListener.php @@ -26,14 +26,6 @@ public function __construct( #[AsEventListener] public function onLayoutEvent(LayoutEvent $event): void { - if ('regular' !== $event->getPage()->type) { - return; - } - - if ($event->getLayout()->customOption) { - $event->getTemplate()->set('customAttribute', 'Lorem Ipsum'); - } - if (!$this->configurationHelper->isEnabledOnPage($event->getPage(), $event->getLayout())) { return; } From e8bb1139496519317b78c627136f6a9d8f8768f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Tue, 21 Apr 2026 16:18:24 +0200 Subject: [PATCH 6/7] run qa --- CHANGELOG.md | 2 +- phpstan-baseline.neon | 6 +++--- src/EntryPoint/EntryPoint.php | 2 -- src/EntryPoint/EntryPointsBuilder.php | 6 ++---- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 265ea12..7c6dd19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,13 @@ All notable changes to this project will be documented in this file. - Added: support for modern twig layouts of contao 5.7 ([#34](https://github.com/heimrichhannot/contao-encore-bundle/pull/34)) - Added: EntrypointsBuilder concept for retriving current page entrypoints ([#34](https://github.com/heimrichhannot/contao-encore-bundle/pull/34)) - Added: support for symfony debug bar ([#35](https://github.com/heimrichhannot/contao-encore-bundle/pull/35)) +- Added: ResponseContext bag for entries ([#36](https://github.com/heimrichhannot/contao-encore-bundle/pull/36)) - Changed: add constant for default field name ([#34](https://github.com/heimrichhannot/contao-encore-bundle/pull/34)) - Fixed: removed dead or unnecessary code and checks ([#34](https://github.com/heimrichhannot/contao-encore-bundle/pull/34)) - Deprecated: `src/Asset/PageEntrypoints.php`, `src/Asset/TemplateAsset.php` and `src/Asset/TemplateAssetGenerator.php` ([#34](https://github.com/heimrichhannot/contao-encore-bundle/pull/34)) - Deprecated: `ConfigurationHelper::isEnabledOnPage()` ([#34](https://github.com/heimrichhannot/contao-encore-bundle/pull/34)) - Deprecated: getter-Methods in `EncoreEnabledEvent` ([#34](https://github.com/heimrichhannot/contao-encore-bundle/pull/34)) -- Added: ResponseContext bag for entries ([#36](https://github.com/heimrichhannot/contao-encore-bundle/pull/36)) ## [2.1.1] - 2026-03-30 - Fixed: compatibility issue with symfony 7 diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index d5fcc66..b2a0643 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3,19 +3,19 @@ parameters: - message: '#^Call to method getLayout\(\) on an unknown class Contao\\CoreBundle\\Event\\LayoutEvent\.$#' identifier: class.notFound - count: 3 + count: 2 path: src/EventListener/InjectPageEntriesListener.php - message: '#^Call to method getPage\(\) on an unknown class Contao\\CoreBundle\\Event\\LayoutEvent\.$#' identifier: class.notFound - count: 3 + count: 2 path: src/EventListener/InjectPageEntriesListener.php - message: '#^Call to method getTemplate\(\) on an unknown class Contao\\CoreBundle\\Event\\LayoutEvent\.$#' identifier: class.notFound - count: 3 + count: 2 path: src/EventListener/InjectPageEntriesListener.php - diff --git a/src/EntryPoint/EntryPoint.php b/src/EntryPoint/EntryPoint.php index fba9078..e49910b 100644 --- a/src/EntryPoint/EntryPoint.php +++ b/src/EntryPoint/EntryPoint.php @@ -2,8 +2,6 @@ namespace HeimrichHannot\EncoreBundle\EntryPoint; -use HeimrichHannot\EncoreBundle\Request\ResponseContext\Entry; - class EntryPoint { public function __construct( diff --git a/src/EntryPoint/EntryPointsBuilder.php b/src/EntryPoint/EntryPointsBuilder.php index e3cb274..7903f08 100644 --- a/src/EntryPoint/EntryPointsBuilder.php +++ b/src/EntryPoint/EntryPointsBuilder.php @@ -54,6 +54,7 @@ public function setResponseContext(?ResponseContext $responseContext): self public function setCustomBag(?EntryBag $entryBag): self { $this->entryBag = $entryBag; + return $this; } @@ -67,10 +68,7 @@ public function build(): EntryPoints $this->available = $available; if ($this->responseContext && $this->responseContext->has(EntryBag::class)) { - $bag = $this->responseContext->get(EntryBag::class); - if ($bag instanceof EntryBag) { - $this->addFromBag($entryPoints, $bag); - } + $this->addFromBag($entryPoints, $this->responseContext->get(EntryBag::class)); } if ($this->pageModel && !$this->layout) { From 3b71450f0ea30051eec881a64a710c647ba36214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Tue, 21 Apr 2026 16:22:15 +0200 Subject: [PATCH 7/7] fix tests and doc --- docs/developers.md | 1 + tests/EntryPoint/EntryPointsBuilderTest.php | 5 ++--- tests/EventListener/InjectPageEntriesListenerTest.php | 11 ++++++----- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/developers.md b/docs/developers.md index 46cf746..f6e256c 100644 --- a/docs/developers.md +++ b/docs/developers.md @@ -69,6 +69,7 @@ To collect or render assets in custom templates or abstinent from the normal pag namespace App\CustomController; +use Contao\CoreBundle\Routing\ResponseContext\ResponseContext; use HeimrichHannot\EncoreBundle\EntryPoint\EntryPointBuilderFactory; use HeimrichHannot\EncoreBundle\Request\ResponseContext\Entry; use HeimrichHannot\EncoreBundle\Request\ResponseContext\EntryBag; diff --git a/tests/EntryPoint/EntryPointsBuilderTest.php b/tests/EntryPoint/EntryPointsBuilderTest.php index 6e49531..e38cfbf 100644 --- a/tests/EntryPoint/EntryPointsBuilderTest.php +++ b/tests/EntryPoint/EntryPointsBuilderTest.php @@ -142,17 +142,16 @@ public function testBuildCombinesFrontendLayoutAndPageEntries(): void $active = $result->allActive(); $this->assertSame( - ['frontend-entry', 'missing-frontend-entry', 'layout-entry', 'shared-entry', 'parent-entry', 'page-entry'], + ['frontend-entry', 'layout-entry', 'shared-entry', 'parent-entry', 'page-entry'], array_keys($all) ); $this->assertSame( - ['frontend-entry', 'missing-frontend-entry', 'layout-entry', 'parent-entry', 'page-entry'], + ['frontend-entry', 'layout-entry', 'parent-entry', 'page-entry'], array_keys($active) ); $this->assertSame('frontend', $all['frontend-entry']->origin); $this->assertFalse($all['frontend-entry']->requiresCss); - $this->assertSame('App', $all['missing-frontend-entry']->extension); $this->assertTrue($all['layout-entry']->head); $this->assertTrue($all['layout-entry']->requiresCss); $this->assertSame('tl_layout.5', $all['layout-entry']->origin); diff --git a/tests/EventListener/InjectPageEntriesListenerTest.php b/tests/EventListener/InjectPageEntriesListenerTest.php index 869c732..57ff1cc 100644 --- a/tests/EventListener/InjectPageEntriesListenerTest.php +++ b/tests/EventListener/InjectPageEntriesListenerTest.php @@ -179,8 +179,6 @@ public function ping(string $value): string $listener->onLayoutEvent(new LayoutEvent($template, $page, $layout)); - $this->assertSame('Lorem Ipsum', $template->get('customAttribute')); - $wrappedResponseContext = $template->get('response_context'); $this->assertSame('other-value', $wrappedResponseContext->other); $this->assertTrue(isset($wrappedResponseContext->end_of_head)); @@ -194,13 +192,16 @@ public function ping(string $value): string $this->assertSame([''], $GLOBALS['TL_BODY']); } - public function testOnLayoutEventReturnsEarlyForNonRegularPages(): void + public function testOnLayoutEventReturnsEarlyWhenDisabled(): void { $page = $this->mockModelObject(PageModel::class, ['type' => 'error_404']); $layout = $this->mockClassWithProperties(LayoutModel::class, ['customOption' => false]); $configurationHelper = $this->createMock(ConfigurationHelper::class); - $configurationHelper->expects($this->never())->method('isEnabledOnPage'); + $configurationHelper->expects($this->once()) + ->method('isEnabledOnPage') + ->with($page, $layout) + ->willReturn(false); $template = new LayoutTemplate('layout', static fn () => new Response()); @@ -210,6 +211,6 @@ public function testOnLayoutEventReturnsEarlyForNonRegularPages(): void $listener->onLayoutEvent(new LayoutEvent($template, $page, $layout)); - $this->assertFalse($template->has('customAttribute')); + $this->assertFalse($template->has('response_context')); } }