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'));
+ }
+}