diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b0033a..7c6dd19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,14 @@ 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)) + ## [2.1.1] - 2026-03-30 - Fixed: compatibility issue with symfony 7 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..f6e256c 100644 --- a/docs/developers.md +++ b/docs/developers.md @@ -69,27 +69,30 @@ To collect or render assets in custom templates or abstinent from the normal pag namespace App\CustomController; -use HeimrichHannot\EncoreBundle\Asset\FrontendAsset; +use Contao\CoreBundle\Routing\ResponseContext\ResponseContext; 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/phpstan-baseline.neon b/phpstan-baseline.neon index 3f32295..b2a0643 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -12,6 +12,12 @@ parameters: count: 2 path: src/EventListener/InjectPageEntriesListener.php + - + message: '#^Call to method getTemplate\(\) on an unknown class Contao\\CoreBundle\\Event\\LayoutEvent\.$#' + identifier: class.notFound + count: 2 + path: src/EventListener/InjectPageEntriesListener.php + - message: '#^Parameter \$event of method HeimrichHannot\\EncoreBundle\\EventListener\\InjectPageEntriesListener\:\:onLayoutEvent\(\) has invalid type Contao\\CoreBundle\\Event\\LayoutEvent\.$#' identifier: class.notFound diff --git a/src/Asset/FrontendAsset.php b/src/Asset/FrontendAsset.php index 31397b4..9a9ff3c 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, __METHOD__); + } + + $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/EntryPoint/EntryPointsBuilder.php b/src/EntryPoint/EntryPointsBuilder.php index dd83180..7903f08 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 = null; public function __construct( private readonly Utils $utils, @@ -42,9 +44,16 @@ 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; } @@ -58,14 +67,8 @@ 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 && $this->responseContext->has(EntryBag::class)) { + $this->addFromBag($entryPoints, $this->responseContext->get(EntryBag::class)); } if ($this->pageModel && !$this->layout) { @@ -105,9 +108,25 @@ 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) { + $this->addEntryPoint( + entryPoints: $entryPoints, + name: $entry->name, + origin: $entry->origin, + extension: $entry->extension, + ); + } + } + 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 d6d6bb7..126f7ab 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, ) { } @@ -30,31 +30,74 @@ public function onLayoutEvent(LayoutEvent $event): void 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(); + $entryPoints = $this->entrypointBuilderFactory->create() + ->setPage($event->getPage()) + ->setLayout($event->getLayout()) + ->setResponseContext($this->responseContextAccessor->getResponseContext()) + ->build(); - if ($request = $this->requestStack->getCurrentRequest()) { - $request->attributes->add([ - 'encore_entries' => $entryPoints, - ]); + 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 ''; + }; + + $responseContext = $event->getTemplate()->get('response_context'); + if (!is_object($responseContext)) { + $loader(); + + 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; + } +} 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..e38cfbf 100644 --- a/tests/EntryPoint/EntryPointsBuilderTest.php +++ b/tests/EntryPoint/EntryPointsBuilderTest.php @@ -2,15 +2,18 @@ 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; -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; @@ -118,13 +121,17 @@ public function testBuildCombinesFrontendLayoutAndPageEntries(): void ->method('model') ->willReturn($modelUtil); - $frontendAsset = new FrontendAsset(); - $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(); @@ -143,7 +150,7 @@ public function testBuildCombinesFrontendLayoutAndPageEntries(): void 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->assertTrue($all['layout-entry']->head); $this->assertTrue($all['layout-entry']->requiresCss); 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 new file mode 100644 index 0000000..57ff1cc --- /dev/null +++ b/tests/EventListener/InjectPageEntriesListenerTest.php @@ -0,0 +1,216 @@ +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 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['globalContaoAsset'] ?? $this->createMock(GlobalContaoAsset::class), + $parameters['configurationHelper'] ?? $this->createMock(ConfigurationHelper::class), + $parameters['requestStack'] ?? $this->createMock(RequestStack::class), + $parameters['responseContextAccessor'] ?? $this->createResponseContextAccessor(), + ); + } + + 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)); + + $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('setResponseContext')->with($responseContextState)->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, + 'responseContextAccessor' => $this->createResponseContextAccessor($responseContextState), + ]); + + $listener->onLayoutEvent(new LayoutEvent($template, $page, $layout)); + + $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 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->once()) + ->method('isEnabledOnPage') + ->with($page, $layout) + ->willReturn(false); + + $template = new LayoutTemplate('layout', static fn () => new Response()); + + $listener = $this->createTestInstance([ + 'configurationHelper' => $configurationHelper, + ]); + + $listener->onLayoutEvent(new LayoutEvent($template, $page, $layout)); + + $this->assertFalse($template->has('response_context')); + } +}