diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51d1b7d..3fe8d1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,16 +1,25 @@ name: CI -on: [ push ] +on: + push: + branches: + - main + - v2 + pull_request: + types: [ opened, reopened, synchronize, ready_for_review ] + branches: + - '**' jobs: tests: name: PHP ${{ matrix.php }} runs-on: ubuntu-latest + if: github.event.pull_request.draft == false strategy: fail-fast: false matrix: php: [ 8.1, 8.2, 8.3, 8.4 ] - contao: [ 4.13.*, 5.3.*, 5.4.* ] + contao: [ 4.13.*, 5.3.*] steps: - name: Setup PHP @@ -34,6 +43,7 @@ jobs: coverage: runs-on: ubuntu-latest + if: github.event.pull_request.draft == false steps: - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -61,6 +71,7 @@ jobs: phpstan: runs-on: ubuntu-latest + if: github.event.pull_request.draft == false steps: - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -79,6 +90,7 @@ jobs: ecs: name: ECS runs-on: ubuntu-latest + if: github.event.pull_request.draft == false steps: - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.gitignore b/.gitignore index 4972bf1..02ff242 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ composer.json~ composer.phar .ddev/ .phpunit.result.cache -phpunit.xml.dist.bak \ No newline at end of file +phpunit.xml.dist.bak +.codex \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 474271e..39fef55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. +## [2.2.0] - 2026-04-20 +- 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)) +- 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/composer.json b/composer.json index fc205e2..3e63f58 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,7 @@ "symfony/config": "^5.4 || ^6.0 || ^7.0", "symfony/console": "^5.4 || ^6.0 || ^7.0", "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", + "symfony/deprecation-contracts": "^1.0 || ^2.0 || ^3.0", "symfony/filesystem": "^5.4 || ^6.0 || ^7.0", "symfony/http-foundation": "^5.4 || ^6.0 || ^7.0", "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0", @@ -36,15 +37,16 @@ "twig/twig": "^1.38.3 || ^2.7 || ^3.0" }, "require-dev": { - "contao/test-case": "^4.0 || ^5.0", + "contao/core-bundle": "^4.13", + "contao/test-case": "^4.0", "contao/manager-plugin": "^2.13", "phpunit/phpunit": "^8.0 || ^9.0", "php-coveralls/php-coveralls": "^2.0", "symfony/phpunit-bridge": "^3.2 || ^4.0 || ^5.0 || ^6.0", "heimrichhannot/contao-test-utilities-bundle": "^0.1.4", - "phpstan/phpstan": "^1.12", - "phpstan/phpstan-symfony": "^1.4", - "rector/rector": "^1.2", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-symfony": "^1.4 || ^2.0", + "rector/rector": "^1.2 || ^2.0", "contao/contao-rector": "dev-main", "symplify/easy-coding-standard": "^12.5" }, diff --git a/config/services.php b/config/services.php new file mode 100644 index 0000000..c3a3e72 --- /dev/null +++ b/config/services.php @@ -0,0 +1,53 @@ +services(); + + $services + ->defaults() + ->autowire() + ->bind('$bundleConfig', '%huh_encore%') + ->bind('$webDir', '%contao.web_dir%') + ->bind('$encoreCache', service('webpack_encore.cache')) + ->bind(CacheItemPoolInterface::class, service('webpack_encore.cache')) + ->bind(TagRenderer::class, service('webpack_encore.tag_renderer')) + ; + + $services + ->load('HeimrichHannot\\EncoreBundle\\', '../src/{Asset,Collection,Command,DataContainer,EventListener,Helper}/*') + ->exclude('../src/Asset/{EntrypointCollection.php}') + ->public() + ->autoconfigure() + ; + + $services + ->set(EntryPointBuilderFactory::class) + ; + + $services + ->alias('huh.encore.asset.frontend', FrontendAsset::class) + ->public() + ; + + $services + ->alias('huh.encore.asset.template', TemplateAsset::class) + ->public() + ; +}; diff --git a/config/services.yml b/config/services.yml deleted file mode 100644 index d418eaf..0000000 --- a/config/services.yml +++ /dev/null @@ -1,22 +0,0 @@ -services: - _defaults: - autowire: true - bind: - $bundleConfig: '%huh_encore%' - $webDir: '%contao.web_dir%' - $encoreCache: '@webpack_encore.cache' - Psr\Cache\CacheItemPoolInterface: "@webpack_encore.cache" - - HeimrichHannot\EncoreBundle\: - resource: "../src/{Asset,Collection,Command,DataContainer,EventListener,Helper}/*" - exclude: '../src/Asset/{EntrypointCollection.php}' - public: true - autoconfigure: true - - huh.encore.asset.frontend: - alias: HeimrichHannot\EncoreBundle\Asset\FrontendAsset - public: true - - huh.encore.asset.template: - alias: HeimrichHannot\EncoreBundle\Asset\TemplateAsset - public: true \ No newline at end of file diff --git a/contao/dca/tl_layout.php b/contao/dca/tl_layout.php index b50a07b..6bb5416 100644 --- a/contao/dca/tl_layout.php +++ b/contao/dca/tl_layout.php @@ -27,7 +27,7 @@ /* * Subpalettes */ -$dca['subpalettes']['addEncore'] = 'encoreEntries,encoreStylesheetsImportsTemplate,encoreScriptsImportsTemplate'; +$dca['subpalettes']['addEncore'] = EncoreEntriesSelectField::NAME_DEFAULT . ',encoreStylesheetsImportsTemplate,encoreScriptsImportsTemplate'; /** * Fields. diff --git a/docs/developers.md b/docs/developers.md index 1223ccf..6849788 100644 --- a/docs/developers.md +++ b/docs/developers.md @@ -4,8 +4,6 @@ This document contains additional information for developers working with encore ## Add entries from your code (frontend module, content element,...) -Since version 1.3 it is possible to add encore entries from your code. So for example the slider assets are automatically included, if the slider module is added to the page. - The most simple method is to use the `PageAssetsTrait` of [Contao Encore Contracts](https://github.com/heimrichhannot/contao-encore-contracts). Use this trait in your class in combination with `ServiceSubscriberInterface` and make sure your class is registered as service with autoconfigure activated. Now you have a new method `addPageEntrypoint()` available. @@ -64,41 +62,58 @@ PaletteManipulator::create() ## Add encore entries to custom template -If you don't want to render assets on page basis, it is possible to generate a custom set of encore entries. - -1. Create an `EntrypointCollection` with the `EntrypointCollectionFactory` service -1. Get your assets with `TemplateAssetGenerator` service. -1. Optional: If you want an input field in the contao backend to select entries, you can use the `DcaGenerator` service to generate an input like on layout or page settings. +To collect or render assets in custom templates or abstinent from the normal page rendering, use the `EntryPointsBuilder`. ```php -use Contao\FrontendTemplate; -use HeimrichHannot\EncoreBundle\Asset\EntrypointCollectionFactory; -use HeimrichHannot\EncoreBundle\Asset\TemplateAssetGenerator; +createCollection($entrypoints); - $template->stylesheets = $templateAssetGenerator->linkTags($collection); - $template->headJavaScript = $templateAssetGenerator->headScriptTags($collection); - $template->javaScript = $templateAssetGenerator->scriptTags($collection); - return $template->getResponse(); -} -``` - -It is also possible to get the stylesheets inline: +namespace App\CustomController; -```php -use Contao\FrontendTemplate; -use HeimrichHannot\EncoreBundle\Asset\EntrypointCollectionFactory; -use HeimrichHannot\EncoreBundle\Asset\TemplateAssetGenerator; +use HeimrichHannot\EncoreBundle\Asset\FrontendAsset; +use HeimrichHannot\EncoreBundle\EntryPoint\EntryPointBuilderFactory; +use Symfony\Component\EventDispatcher\Attribute\AsEventListener; +use Symfony\Component\HttpFoundation\Response; +use Symfony\WebpackEncoreBundle\Asset\TagRenderer;use Twig\Environment; -function renderTemplateWithEncore(array $entrypoints, EntrypointCollectionFactory $entrypointCollectionFactory, TemplateAssetGenerator $templateAssetGenerator) +class CustomController { - $template = new FrontendTemplate(); - $collection = $entrypointCollectionFactory->createCollection($entrypoints); - $template->inlineCss = $templateAssetGenerator->inlineCssLinkTag($collection); - return $template->getResponse(); + private readonly TagRenderer $tagRenderer; + private readonly EntryPointBuilderFactory $entrypointBuilderFactory; + private readonly Environment $twig; + private readonly FrontendAsset $frontendAsset; + + public function __invoke(): 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) + // build the collection: + ->build(); + + // render the tags, for example with the tag renderer of webpack encore bundle + $this->tagRenderer->reset(); + $css = $head = $body = []; + foreach ($entryPoints->allActive() as $entrypoint) { + if ($entrypoint->requiresCss) { + $css[] = $this->tagRenderer->renderWebpackLinkTags($entrypoint->name); + } + if ($entrypoint->head) { + $head[] = $this->tagRenderer->renderWebpackScriptTags($entrypoint->name); + } else { + $body[] = $this->tagRenderer->renderWebpackScriptTags($entrypoint->name); + } + } + + // render the template + return new Response($this->twig->render('custom_template.html.twig', [ + 'css' => $css, + 'head' => $head, + 'body' => $body, + ])); + } } ``` @@ -106,23 +121,8 @@ function renderTemplateWithEncore(array $entrypoints, EntrypointCollectionFactor The `ConfigurationHelper` service can be used to obtain some configuration information. Following methods are available: -`isEnabledOnCurrentPage(?PageModel $pageModel = null): bool` - Return if encore is enabled for the current frontend page. You can pass a page object to check for a custom page, otherweise `global $objPage` is used. +`isEnabledOnPage(PageModel $page, ?LayoutModel $layout = null): bool` - Return if encore is enabled for the current frontend page. `getRelativeOutputPath(): string` - Return the relative output path configured by webpack encore bundle. Typical this is `build`. -`getAbsoluteOutputPath(): string` - Return the absolute output path configured by webpack encore bundle. For example `/var/www/html/project/web/build` - -## Custom import templates - -If you need custom templates for the import of javascript and stylesheet assets files, Encore Bundle provide support for this. -Create a twig template (see `src/Resources/views` for examples) and register them in your (project) bundle config. - -Example: - -```yaml -huh_encore: - templates: - imports: - - { name: default_css, template: "@HeimrichHannotEncore/encore_css_imports.html.twig" } - - { name: default_js, template: "@HeimrichHannotEncore/encore_js_imports.html.twig" } -``` \ No newline at end of file +`getAbsoluteOutputPath(): string` - Return the absolute output path configured by webpack encore bundle. For example `/var/www/html/project/public/build` \ No newline at end of file diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a0a9f7f..3f32295 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,6 +1,19 @@ parameters: ignoreErrors: - - message: "#^Offset 'fields' on array\\{\\} in isset\\(\\) does not exist\\.$#" + message: '#^Call to method getLayout\(\) on an unknown class Contao\\CoreBundle\\Event\\LayoutEvent\.$#' + identifier: class.notFound count: 2 - path: tests/EventListener/DcaField/EncoreEntriesSelectFieldListenerTest.php + path: src/EventListener/InjectPageEntriesListener.php + + - + message: '#^Call to method getPage\(\) 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 + count: 1 + path: src/EventListener/InjectPageEntriesListener.php diff --git a/phpstan.neon b/phpstan.neon index e630b1c..3a4bf3f 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,8 +1,7 @@ parameters: - level: 4 + level: 5 paths: - src - - tests - contao universalObjectCratesClasses: - Contao\LayoutModel diff --git a/rector.php b/rector.php index 77d1b97..d31bad3 100644 --- a/rector.php +++ b/rector.php @@ -5,30 +5,42 @@ use Contao\Rector\Set\ContaoLevelSetList; use Contao\Rector\Set\ContaoSetList; use Rector\Config\RectorConfig; +use Rector\Php81\Rector\Array_\ArrayToFirstClassCallableRector; use Rector\Php84\Rector\Param\ExplicitNullableParamTypeRector; use Rector\Set\ValueObject\LevelSetList; use Rector\Symfony\Set\SymfonySetList; use Rector\TypeDeclaration\Rector\ClassMethod\AddVoidReturnTypeWhereNoReturnRector; +use Rector\ValueObject\PhpVersion; return RectorConfig::configure() ->withPaths([ __DIR__ . '/src', __DIR__ . '/contao', + __DIR__ . '/config', ]) + ->withPhpVersion(PhpVersion::PHP_84) ->withRules([ AddVoidReturnTypeWhereNoReturnRector::class, - # In Vorbereitung für PHP 8.4: ExplicitNullableParamTypeRector::class ]) - ->withImportNames(importShortClasses: false, removeUnusedImports: true) + ->withImportNames( + importShortClasses: false, + removeUnusedImports: true, + ) + ->withComposerBased( + twig: true, + doctrine: true, + phpunit: true, + symfony: true, + ) ->withSets([ LevelSetList::UP_TO_PHP_81, - SymfonySetList::SYMFONY_54, - SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION, - # Erst mit Symfony 6 (Contao 5) nutzen: - // SymfonySetList::ANNOTATIONS_TO_ATTRIBUTES, ContaoLevelSetList::UP_TO_CONTAO_413, ContaoSetList::FQCN, ContaoSetList::ANNOTATIONS_TO_ATTRIBUTES, - ]); \ No newline at end of file + ]) + ->withSkip([ + ArrayToFirstClassCallableRector::class, + ]) + ; \ No newline at end of file diff --git a/src/Asset/PageEntrypoints.php b/src/Asset/PageEntrypoints.php index 6d24403..8fecf24 100644 --- a/src/Asset/PageEntrypoints.php +++ b/src/Asset/PageEntrypoints.php @@ -16,6 +16,9 @@ use HeimrichHannot\EncoreBundle\Helper\ArrayHelper; use HeimrichHannot\UtilsBundle\Util\Utils; +/** + * @deprecated Since version 2.2 + */ class PageEntrypoints { protected $jsEntries = []; @@ -81,9 +84,7 @@ public function collectPageEntries(LayoutModel $layout, PageModel $currentPage, $parents = [$layout]; $parentPages = $this->utils->model()->findParentsRecursively($currentPage, 'pid'); - if (\is_array($parentPages)) { - $parents = array_merge($parents, $parentPages); - } + $parents = array_merge($parents, $parentPages); $parents = array_merge($parents, [$currentPage]); $parents = array_reverse($parents); diff --git a/src/Asset/TemplateAsset.php b/src/Asset/TemplateAsset.php index a194423..52a9e27 100644 --- a/src/Asset/TemplateAsset.php +++ b/src/Asset/TemplateAsset.php @@ -14,6 +14,9 @@ use Twig\Environment; use Twig\Error\RuntimeError; +/** + * @deprecated Since version 2.2 + */ class TemplateAsset { /** diff --git a/src/Asset/TemplateAssetGenerator.php b/src/Asset/TemplateAssetGenerator.php index 2897e72..5edb7c5 100644 --- a/src/Asset/TemplateAssetGenerator.php +++ b/src/Asset/TemplateAssetGenerator.php @@ -14,6 +14,9 @@ use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; +/** + * @deprecated Since 2.2 + */ class TemplateAssetGenerator { /** diff --git a/src/ContaoManager/Plugin.php b/src/ContaoManager/Plugin.php index 2b6bf5c..0eda75b 100644 --- a/src/ContaoManager/Plugin.php +++ b/src/ContaoManager/Plugin.php @@ -33,6 +33,6 @@ public function getBundles(ParserInterface $parser): array public function registerContainerConfiguration(LoaderInterface $loader, array $managerConfig): void { $loader->load('@HeimrichHannotEncoreBundle/config/config.yml'); - $loader->load('@HeimrichHannotEncoreBundle/config/services.yml'); + $loader->load('@HeimrichHannotEncoreBundle/config/services.php'); } } diff --git a/src/Dca/EncoreEntriesSelectField.php b/src/Dca/EncoreEntriesSelectField.php index ea4b1ad..721a2fd 100644 --- a/src/Dca/EncoreEntriesSelectField.php +++ b/src/Dca/EncoreEntriesSelectField.php @@ -4,6 +4,8 @@ class EncoreEntriesSelectField { + public const NAME_DEFAULT = 'encoreEntries'; + protected static array $tables = []; /** diff --git a/src/Dca/EncoreEntriesSelectFieldOptions.php b/src/Dca/EncoreEntriesSelectFieldOptions.php index 2abfa75..091df03 100644 --- a/src/Dca/EncoreEntriesSelectFieldOptions.php +++ b/src/Dca/EncoreEntriesSelectFieldOptions.php @@ -4,7 +4,7 @@ class EncoreEntriesSelectFieldOptions { - protected string $fieldName = 'encoreEntries'; + protected string $fieldName = EncoreEntriesSelectField::NAME_DEFAULT; protected bool $includeActiveCheckbox = false; protected ?array $fieldLabel = null; protected ?array $selectLabel = null; diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index ec9ff12..60836c0 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -20,6 +20,7 @@ public function getConfigTreeBuilder(): TreeBuilder $treeBuilder->getRootNode() ->children() ->arrayNode('templates') + ->setDeprecated('heimrichhannot/contao-encore-bundle', '2.2.0') ->addDefaultsIfNotSet() ->children() ->arrayNode('imports') diff --git a/src/EntryPoint/EntryPoint.php b/src/EntryPoint/EntryPoint.php new file mode 100644 index 0000000..e49910b --- /dev/null +++ b/src/EntryPoint/EntryPoint.php @@ -0,0 +1,16 @@ +utils, + $this->entryCollection, + ); + } +} diff --git a/src/EntryPoint/EntryPoints.php b/src/EntryPoint/EntryPoints.php new file mode 100644 index 0000000..f763773 --- /dev/null +++ b/src/EntryPoint/EntryPoints.php @@ -0,0 +1,41 @@ +entryPoints[$entryPoint->name] = $entryPoint; + if ($entryPoint->active) { + $this->active[$entryPoint->name] = $entryPoint; + } else { + unset($this->active[$entryPoint->name]); + } + } + + /** + * @return EntryPoint[] + */ + public function all(): array + { + return $this->entryPoints; + } + + /** + * @return EntryPoint[] + */ + public function allActive(): array + { + return $this->active; + } +} diff --git a/src/EntryPoint/EntryPointsBuilder.php b/src/EntryPoint/EntryPointsBuilder.php new file mode 100644 index 0000000..dd83180 --- /dev/null +++ b/src/EntryPoint/EntryPointsBuilder.php @@ -0,0 +1,130 @@ +pageModel = $page; + $this->pageField = $field; + + return $this; + } + + public function setLayout(?LayoutModel $layout, string $field = EncoreEntriesSelectField::NAME_DEFAULT): self + { + $this->layout = $layout; + $this->layoutField = $field; + + return $this; + } + + public function setFrontendAsset(?FrontendAsset $frontendAsset): self + { + $this->frontendAsset = $frontendAsset; + + return $this; + } + + public function build(): EntryPoints + { + $entryPoints = new EntryPoints(); + $available = $this->entryCollection->getEntries(); + if ([] !== $available) { + $available = array_combine(array_column($available, 'name'), $available); + } + $this->available = $available; + + if ($this->frontendAsset) { + foreach ($this->frontendAsset->getActiveEntrypoints() as $entryPoint) { + $this->addEntryPoint( + entryPoints: $entryPoints, + name: $entryPoint, + origin: FrontendAsset::class, + ); + } + } + + if ($this->pageModel && !$this->layout) { + $this->pageModel->loadDetails(); + $layout = LayoutModel::findByPk($this->pageModel->layoutId ?? $this->pageModel->layout); + if ($layout) { + $this->setLayout($layout); + } + } + + if ($this->layout) { + foreach (StringUtil::deserialize($this->layout->{$this->layoutField}, true) as $entrypoint) { + $this->addEntryPoint( + entryPoints: $entryPoints, + name: $entrypoint['entry'] ?? '', + active: (bool) ($entrypoint['active'] ?? true), + origin: 'tl_layout.' . $this->layout->id, + extension: 'App', + ); + } + } + + if (null !== $this->pageModel) { + $pages = $this->utils->model()->findParentsRecursively($this->pageModel, 'pid'); + $pages[] = $this->pageModel; + + foreach ($pages as $page) { + foreach (StringUtil::deserialize($page->{$this->pageField}, true) as $entrypoint) { + $this->addEntryPoint( + entryPoints: $entryPoints, + name: $entrypoint['entry'] ?? '', + active: (bool) ($entrypoint['active'] ?? true), + origin: 'tl_page.' . $page->id, + extension: 'App', + ); + } + } + } + + return $entryPoints; + } + + private function addEntryPoint(EntryPoints $entryPoints, string $name, bool $active = true, string $origin = '', string $extension = ''): void + { + if ('' === $name) { + return; + } + + if (!isset($this->available[$name])) { + return; + } + + $entryPoints->add(new EntryPoint( + name: $name, + active: $active, + head: $this->available[$name]['head'] ?? false, + requiresCss: (bool) ($this->available[$name]['requires_css'] ?? true), + origin: $origin, + extension: $extension, + )); + } +} diff --git a/src/Event/EncoreEnabledEvent.php b/src/Event/EncoreEnabledEvent.php index 0c2e9fc..d8085ef 100644 --- a/src/Event/EncoreEnabledEvent.php +++ b/src/Event/EncoreEnabledEvent.php @@ -8,6 +8,7 @@ namespace HeimrichHannot\EncoreBundle\Event; +use Contao\LayoutModel; use Contao\PageModel; use Symfony\Component\HttpFoundation\Request; use Symfony\Contracts\EventDispatcher\Event; @@ -15,14 +16,24 @@ class EncoreEnabledEvent extends Event { public function __construct( - private bool $enabled, - private readonly Request $request, - private readonly ?PageModel $pageModel, + public bool $enabled, + public readonly Request $request, + public readonly ?PageModel $pageModel = null, + public readonly ?LayoutModel $layoutModel = null, ) { } + /** + * @deprecated + */ public function isEnabled(): bool { + trigger_deprecation( + 'heimrichhannot/contao-encore-bundle', + '2.2.0', + 'Use class properties instead.' + ); + return $this->enabled; } @@ -33,13 +44,31 @@ public function setEnabled(bool $enabled): self return $this; } + /** + * @deprecated + */ public function getRequest(): Request { + trigger_deprecation( + 'heimrichhannot/contao-encore-bundle', + '2.2.0', + 'Use class properties instead.' + ); + return $this->request; } + /** + * @deprecated + */ public function getPageModel(): ?PageModel { + trigger_deprecation( + 'heimrichhannot/contao-encore-bundle', + '2.2.0', + 'Use class properties instead.' + ); + return $this->pageModel; } } diff --git a/src/EventListener/Contao/ReplaceDynamicScriptTagsListener.php b/src/EventListener/Contao/ReplaceDynamicScriptTagsListener.php index d62c492..4f49303 100644 --- a/src/EventListener/Contao/ReplaceDynamicScriptTagsListener.php +++ b/src/EventListener/Contao/ReplaceDynamicScriptTagsListener.php @@ -10,78 +10,64 @@ use Contao\CoreBundle\DependencyInjection\Attribute\AsHook; use Contao\CoreBundle\Framework\ContaoFramework; -use Contao\LayoutModel; -use Contao\PageModel; +use HeimrichHannot\EncoreBundle\Asset\FrontendAsset; use HeimrichHannot\EncoreBundle\Asset\GlobalContaoAsset; -use HeimrichHannot\EncoreBundle\Asset\TemplateAsset; +use HeimrichHannot\EncoreBundle\EntryPoint\EntryPointBuilderFactory; use HeimrichHannot\EncoreBundle\Helper\ConfigurationHelper; use HeimrichHannot\UtilsBundle\Util\Utils; +use Symfony\WebpackEncoreBundle\Asset\TagRenderer; #[AsHook('replaceDynamicScriptTags')] class ReplaceDynamicScriptTagsListener { public function __construct( - protected array $bundleConfig, - private readonly ContaoFramework $contaoFramework, private readonly Utils $utils, - protected TemplateAsset $templateAsset, protected ConfigurationHelper $configurationHelper, private readonly GlobalContaoAsset $globalContaoAsset, + private readonly EntryPointBuilderFactory $entryPointBuilderFactory, + private readonly FrontendAsset $frontendAsset, + private readonly TagRenderer $tagRenderer, ) { } public function __invoke(string $buffer): string { - if (!$this->configurationHelper->isEnabledOnCurrentPage()) { - return $buffer; - } - $pageModel = $this->utils->request()->getCurrentPageModel(); if (!$pageModel) { return $buffer; } - $pageModel->loadDetails(); - - if (!($layout = $this->contaoFramework->getAdapter(LayoutModel::class)->findByPk($pageModel->layoutId ?? $pageModel->layout))) { + if (!$this->configurationHelper->isEnabledOnPage($pageModel)) { return $buffer; } - /* @var LayoutModel|null $layout */ - $buffer = $this->replaceContaoTags($buffer, $pageModel, $layout); - $this->globalContaoAsset->cleanGlobalArrayFromConfiguration(); - return $buffer; - } - - protected function replaceEncoreTags(string $buffer, PageModel $page, LayoutModel $layout): string - { - $templateAssets = $this->templateAsset->createInstance($page, $layout, 'encoreEntries'); - - $replace = []; - $replace['[[HUH_ENCORE_CSS]]'] = trim($templateAssets->linkTags()); - // caution: always render head first because of global dependencies like jQuery - $replace['[[HUH_ENCORE_HEAD_JS]]'] = trim($templateAssets->headScriptTags()); - $replace['[[HUH_ENCORE_JS]]'] = trim($templateAssets->scriptTags()); - - return str_replace(array_keys($replace), $replace, $buffer); - } - - protected function replaceContaoTags(string $buffer, PageModel $page, LayoutModel $layout): string - { - $templateAssets = $this->templateAsset->createInstance($page, $layout, 'encoreEntries'); - - $nonce = ''; - if (method_exists(ContaoFramework::class, 'getNonce')) { - $nonce = '_' . ContaoFramework::getNonce(); + $entryPoints = $this->entryPointBuilderFactory->create() + ->setFrontendAsset($this->frontendAsset) + ->setPage($pageModel) + ->build(); + + $css = ''; + $headJs = ''; + $bodyJs = ''; + foreach ($entryPoints->allActive() as $entrypoint) { + if ($entrypoint->requiresCss) { + $css .= $this->tagRenderer->renderWebpackLinkTags($entrypoint->name); + } + if ($entrypoint->head) { + $headJs .= $this->tagRenderer->renderWebpackScriptTags($entrypoint->name); + } else { + $bodyJs .= $this->tagRenderer->renderWebpackScriptTags($entrypoint->name); + } } - $replace = []; - $replace["[[TL_CSS$nonce]]"] = "[[TL_CSS$nonce]]" . trim($templateAssets->linkTags()); + $this->globalContaoAsset->cleanGlobalArrayFromConfiguration(); - // caution: always render head first because of global dependencies like jQuery - $replace["[[TL_HEAD$nonce]]"] = trim($templateAssets->headScriptTags()) . "[[TL_HEAD$nonce]]"; - $replace["[[TL_BODY$nonce]]"] = trim($templateAssets->scriptTags()) . "[[TL_BODY$nonce]]"; + $nonce = '_' . ContaoFramework::getNonce(); + $replace = []; + $replace["[[TL_CSS$nonce]]"] = "[[TL_CSS$nonce]]" . trim($css); + $replace["[[TL_HEAD$nonce]]"] = trim($headJs) . "[[TL_HEAD$nonce]]"; + $replace["[[TL_BODY$nonce]]"] = trim($bodyJs) . "[[TL_BODY$nonce]]"; return str_replace(array_keys($replace), $replace, $buffer); } diff --git a/src/EventListener/InjectPageEntriesListener.php b/src/EventListener/InjectPageEntriesListener.php new file mode 100644 index 0000000..526db6f --- /dev/null +++ b/src/EventListener/InjectPageEntriesListener.php @@ -0,0 +1,52 @@ +configurationHelper->isEnabledOnPage($event->getPage(), $event->getLayout())) { + return; + } + + $this->globalContaoAsset->cleanGlobalArrayFromConfiguration(); + + $entryPoints = $this->entrypointBuilderFactory->create() + ->setPage($event->getPage()) + ->setLayout($event->getLayout()) + ->setFrontendAsset($this->frontendAsset) + ->build(); + + $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); + } + } + } +} diff --git a/src/Helper/ConfigurationHelper.php b/src/Helper/ConfigurationHelper.php index d81f98f..f2c7a58 100644 --- a/src/Helper/ConfigurationHelper.php +++ b/src/Helper/ConfigurationHelper.php @@ -21,49 +21,81 @@ class ConfigurationHelper { - /** - * @var RequestStack - */ - protected $requestStack; - /** - * @var array - */ - protected $bundleConfig; - /** - * @var string - */ - protected $webDir; + protected array $bundleConfig; + protected string $webDir; public function __construct( - RequestStack $requestStack, + private readonly RequestStack $requestStack, ParameterBagInterface $parameterBag, private readonly ScopeMatcher $scopeMatcher, private readonly ContaoFramework $contaoFramework, private readonly EventDispatcherInterface $eventDispatcher, ) { - $this->requestStack = $requestStack; $this->bundleConfig = $parameterBag->has('huh_encore') ? $parameterBag->get('huh_encore') : []; $this->webDir = $parameterBag->has('contao.web_dir') ? $parameterBag->get('contao.web_dir') : ''; } /** * Check if encore is enabled on the current page. + * + * @deprecated */ - public function isEnabledOnCurrentPage(?PageModel $pageModel = null): bool + public function isEnabledOnCurrentPage(?PageModel $pageModel = null, ?LayoutModel $layout = null): bool + { + trigger_deprecation( + 'heimrichhannot/contao-encore-bundle', + '2.2.0', + 'The method "isEnabledOnCurrentPage" is deprecated since version 2.2.0 and will be removed in version 3.0.0. Please use "isEnabledOnPage" instead.' + ); + + $pageModel ??= $this->getPageModel(); + + if (null === $pageModel) { + return false; + } + + return $this->isEnabledOnPage($pageModel, $layout); + } + + public function isEnabledOnPage(PageModel $page, ?LayoutModel $layout = null): bool { $request = $this->requestStack->getCurrentRequest(); + if (!$request || !$this->scopeMatcher->isFrontendRequest($request)) { + return $this->dispatchEvent(false, $request); + } + + if (!$layout) { + $page->loadDetails(); + $layout = $this->contaoFramework + ->getAdapter(LayoutModel::class) + ->findByPk($page->layoutId ?? $page->layout); + } + + if (!$layout?->addEncore) { + return $this->dispatchEvent(false, $request, $page, $layout); + } + + if ('modern' !== $layout->type) { + if (false === $this->evaluateIsEnabled($page)) { + return $this->dispatchEvent(false, $request, $page, $layout); + } + } + + return $this->dispatchEvent(true, $request, $page, $layout); + } + + private function dispatchEvent(bool $result, ?Request $request = null, ?PageModel $page = null, ?LayoutModel $layout = null): bool + { + // ToDo: allow request = null in event if (!$request) { return false; } - $result = $this->evaluateIsEnabled($pageModel, $request); - - /** @var EncoreEnabledEvent $event */ $event = $this->eventDispatcher->dispatch( - new EncoreEnabledEvent($result, $request, $pageModel) + new EncoreEnabledEvent($result, $request, $page, $layout) ); - return $event->isEnabled(); + return $event->enabled; } /** @@ -107,12 +139,8 @@ public function getPageModel(): ?PageModel return $this->contaoFramework->getAdapter(PageModel::class)->findByPk((int) $pageModel); } - private function evaluateIsEnabled(?PageModel $pageModel, Request $request): bool + private function evaluateIsEnabled(?PageModel $pageModel): bool { - if (!$this->scopeMatcher->isFrontendRequest($request)) { - return false; - } - $parentPageModel = $this->getPageModel(); // Check if error page @@ -130,13 +158,6 @@ private function evaluateIsEnabled(?PageModel $pageModel, Request $request): boo return false; } - $pageModel->loadDetails(); - $layout = $this->contaoFramework->getAdapter(LayoutModel::class)->findByPk($pageModel->layoutId ?? $pageModel->layout); - - if (!$layout || !$layout->addEncore) { - return false; - } - return true; } } diff --git a/tests/EntryPoint/EntryPointsBuilderTest.php b/tests/EntryPoint/EntryPointsBuilderTest.php new file mode 100644 index 0000000..2598343 --- /dev/null +++ b/tests/EntryPoint/EntryPointsBuilderTest.php @@ -0,0 +1,161 @@ +createMock(Utils::class); + $entryCollection = $this->createMock(EntryCollection::class); + + $factory = new EntryPointBuilderFactory($utils, $entryCollection); + + $firstBuilder = $factory->create(); + $secondBuilder = $factory->create(); + + $this->assertInstanceOf(EntryPointsBuilder::class, $firstBuilder); + $this->assertInstanceOf(EntryPointsBuilder::class, $secondBuilder); + $this->assertNotSame($firstBuilder, $secondBuilder); + } + + public function testEntryPointsTracksAllAndActiveEntries(): void + { + $entryPoint = new EntryPoint( + name: 'app', + active: true, + head: true, + requiresCss: true, + origin: 'frontend', + extension: 'App', + ); + + $this->assertSame('app', $entryPoint->name); + $this->assertTrue($entryPoint->active); + $this->assertTrue($entryPoint->head); + $this->assertTrue($entryPoint->requiresCss); + $this->assertSame('frontend', $entryPoint->origin); + $this->assertSame('App', $entryPoint->extension); + + $entryPoints = new EntryPoints(); + $entryPoints->add($entryPoint); + $entryPoints->add(new EntryPoint('deferred', head: false, requiresCss: false)); + $entryPoints->add(new EntryPoint('app', active: false, origin: 'tl_page.1')); + + $all = $entryPoints->all(); + $active = $entryPoints->allActive(); + + $this->assertCount(2, $all); + $this->assertCount(1, $active); + $this->assertFalse($all['app']->active); + $this->assertSame('tl_page.1', $all['app']->origin); + $this->assertArrayNotHasKey('app', $active); + $this->assertSame('deferred', $active['deferred']->name); + } + + public function testBuildCombinesFrontendLayoutAndPageEntries(): void + { + $entryCollection = $this->createMock(EntryCollection::class); + $entryCollection->expects($this->once()) + ->method('getEntries') + ->willReturn([ + ['name' => 'frontend-entry', 'requires_css' => false], + ['name' => 'layout-entry', 'head' => true, 'requires_css' => true], + ['name' => 'shared-entry', 'head' => false, 'requires_css' => true], + ['name' => 'parent-entry'], + ['name' => 'page-entry', 'requires_css' => false], + ]); + + $parentPage = $this->mockModelObject(PageModel::class, [ + 'id' => 2, + 'customEntries' => serialize([ + ['entry' => 'shared-entry', 'active' => '1'], + ['entry' => 'parent-entry', 'active' => '1'], + ['entry' => 'missing-entry', 'active' => '1'], + ]), + ]); + + $page = $this->mockModelObject(PageModel::class, [ + 'id' => 3, + 'customEntries' => serialize([ + ['entry' => 'shared-entry', 'active' => ''], + ['entry' => 'page-entry', 'active' => '1'], + ['entry' => '', 'active' => '1'], + ]), + ]); + + $layout = $this->mockClassWithProperties(LayoutModel::class, [ + 'id' => 5, + 'layoutEntries' => serialize([ + ['entry' => 'layout-entry', 'active' => '1'], + ['entry' => 'missing-layout-entry', 'active' => '1'], + ]), + ]); + + $modelUtil = $this->createMock(ModelUtil::class); + $modelUtil->expects($this->once()) + ->method('findParentsRecursively') + ->with($page, 'pid') + ->willReturn([$parentPage]); + + $utils = $this->createMock(Utils::class); + $utils->expects($this->once()) + ->method('model') + ->willReturn($modelUtil); + + $frontendAsset = new FrontendAsset(); + $frontendAsset->addActiveEntrypoint('frontend-entry'); + $frontendAsset->addActiveEntrypoint('missing-frontend-entry'); + + $builder = new EntryPointsBuilder($utils, $entryCollection); + $result = $builder + ->setFrontendAsset($frontendAsset) + ->setLayout($layout, 'layoutEntries') + ->setPage($page, 'customEntries') + ->build(); + + $this->assertInstanceOf(EntryPoints::class, $result); + + $all = $result->all(); + $active = $result->allActive(); + + $this->assertSame( + ['frontend-entry', 'layout-entry', 'shared-entry', 'parent-entry', 'page-entry'], + array_keys($all) + ); + $this->assertSame( + ['frontend-entry', 'layout-entry', 'parent-entry', 'page-entry'], + array_keys($active) + ); + + $this->assertSame(FrontendAsset::class, $all['frontend-entry']->origin); + $this->assertFalse($all['frontend-entry']->requiresCss); + $this->assertTrue($all['layout-entry']->head); + $this->assertTrue($all['layout-entry']->requiresCss); + $this->assertSame('tl_layout.5', $all['layout-entry']->origin); + $this->assertSame('App', $all['layout-entry']->extension); + $this->assertFalse($all['shared-entry']->active); + $this->assertSame('tl_page.3', $all['shared-entry']->origin); + $this->assertArrayNotHasKey('shared-entry', $active); + $this->assertTrue($all['parent-entry']->requiresCss); + $this->assertSame('tl_page.2', $all['parent-entry']->origin); + $this->assertFalse($all['page-entry']->head); + $this->assertFalse($all['page-entry']->requiresCss); + $this->assertSame('tl_page.3', $all['page-entry']->origin); + } +} diff --git a/tests/EventListener/Contao/ReplaceDynamicScriptTagsListenerTest.php b/tests/EventListener/Contao/ReplaceDynamicScriptTagsListenerTest.php index 9c55c63..0400668 100644 --- a/tests/EventListener/Contao/ReplaceDynamicScriptTagsListenerTest.php +++ b/tests/EventListener/Contao/ReplaceDynamicScriptTagsListenerTest.php @@ -9,161 +9,148 @@ namespace HeimrichHannot\EncoreBundle\Test\EventListener\Contao; use Contao\CoreBundle\Framework\ContaoFramework; -use Contao\CoreBundle\ServiceAnnotation\Page; -use Contao\LayoutModel; use Contao\PageModel; use Contao\TestCase\ContaoTestCase; +use HeimrichHannot\EncoreBundle\Asset\FrontendAsset; use HeimrichHannot\EncoreBundle\Asset\GlobalContaoAsset; -use HeimrichHannot\EncoreBundle\Asset\TemplateAsset; +use HeimrichHannot\EncoreBundle\EntryPoint\EntryPoint; +use HeimrichHannot\EncoreBundle\EntryPoint\EntryPointBuilderFactory; +use HeimrichHannot\EncoreBundle\EntryPoint\EntryPoints; +use HeimrichHannot\EncoreBundle\EntryPoint\EntryPointsBuilder; use HeimrichHannot\EncoreBundle\EventListener\Contao\ReplaceDynamicScriptTagsListener; use HeimrichHannot\EncoreBundle\Helper\ConfigurationHelper; use HeimrichHannot\TestUtilitiesBundle\Mock\ModelMockTrait; use HeimrichHannot\UtilsBundle\Util\RequestUtil; use HeimrichHannot\UtilsBundle\Util\Utils; -use PHPUnit\Framework\MockObject\MockBuilder; -use PHPUnit\Framework\MockObject\MockObject; +use Symfony\WebpackEncoreBundle\Asset\TagRenderer; class ReplaceDynamicScriptTagsListenerTest extends ContaoTestCase { use ModelMockTrait; - /** - * @return ReplaceDynamicScriptTagsListener|MockObject - */ - public function createTestInstance(array $parameter = [], ?MockBuilder $mockBuilder = null) + public function createTestInstance(array $parameter = []): ReplaceDynamicScriptTagsListener { - $parameter['bundleConfig'] = $parameter['bundleConfig'] ?? []; - $parameter['contaoFramework'] = $parameter['contaoFramework'] ?? $this->mockContaoFramework(); $parameter['utils'] = $parameter['utils'] ?? $this->createMock(Utils::class); - $parameter['templateAsset'] = $parameter['templateAsset'] ?? $this->createMock(TemplateAsset::class); $parameter['configurationHelper'] = $parameter['configurationHelper'] ?? $this->createMock(ConfigurationHelper::class); $parameter['globalContaoAsset'] = $parameter['globalContaoAsset'] ?? $this->createMock(GlobalContaoAsset::class); - - if ($mockBuilder) { - $instance = $mockBuilder->setConstructorArgs([ - $parameter['bundleConfig'], - $parameter['contaoFramework'], - $parameter['utils'], - $parameter['templateAsset'], - $parameter['configurationHelper'], - $parameter['globalContaoAsset'], - ])->getMock(); - } else { - $instance = new ReplaceDynamicScriptTagsListener( - $parameter['bundleConfig'], - $parameter['contaoFramework'], - $parameter['utils'], - $parameter['templateAsset'], - $parameter['configurationHelper'], - $parameter['globalContaoAsset'], - ); - } - - return $instance; + $parameter['entryPointBuilderFactory'] = $parameter['entryPointBuilderFactory'] ?? $this->createMock(EntryPointBuilderFactory::class); + $parameter['frontendAsset'] = $parameter['frontendAsset'] ?? $this->createMock(FrontendAsset::class); + $parameter['tagRenderer'] = $parameter['tagRenderer'] ?? $this->createMock(TagRenderer::class); + + return new ReplaceDynamicScriptTagsListener( + $parameter['utils'], + $parameter['configurationHelper'], + $parameter['globalContaoAsset'], + entryPointBuilderFactory: $parameter['entryPointBuilderFactory'], + frontendAsset: $parameter['frontendAsset'], + tagRenderer: $parameter['tagRenderer'], + ); } public function testInvoke() { - // - // Encore not enabled - // - - $configurationHelper = $this->createMock(ConfigurationHelper::class); - $configurationHelper->method('isEnabledOnCurrentPage')->willReturn(false); - - $utils = $this->createMock(Utils::class); - $utils->expects($this->never())->method('request'); - - $instance = $this->createTestInstance([ - 'utils' => $utils, - 'configurationHelper' => $configurationHelper, - ]); - $instance->__invoke('test'); - - // - // No page - // - - $configurationHelper = $this->createMock(ConfigurationHelper::class); - $configurationHelper->method('isEnabledOnCurrentPage')->willReturn(true); - $requestUtil = $this->createMock(RequestUtil::class); $requestUtil->method('getCurrentPageModel')->willReturn(null); + $utils = $this->createMock(Utils::class); $utils->method('request')->willReturn($requestUtil); - $layoutAdapter = $this->mockAdapter(['findByPk']); - $layoutAdapter->expects($this->never())->method('findByPk'); + $configurationHelper = $this->createMock(ConfigurationHelper::class); + $configurationHelper->expects($this->never())->method('isEnabledOnPage'); + + $entryPointBuilderFactory = $this->createMock(EntryPointBuilderFactory::class); + $entryPointBuilderFactory->expects($this->never())->method('create'); - $framework = $this->mockContaoFramework([ - LayoutModel::class => $layoutAdapter, - ]); + $globalContaoAsset = $this->createMock(GlobalContaoAsset::class); + $globalContaoAsset->expects($this->never())->method('cleanGlobalArrayFromConfiguration'); $instance = $this->createTestInstance([ 'utils' => $utils, 'configurationHelper' => $configurationHelper, - 'contaoFramework' => $framework, + 'globalContaoAsset' => $globalContaoAsset, + 'entryPointBuilderFactory' => $entryPointBuilderFactory, ]); - $instance->__invoke('test'); + $this->assertSame('test', $instance->__invoke('test')); - // - // No Layout - // + $pageModel = $this->mockModelObject(PageModel::class, [ + 'id' => 1, + ]); $requestUtil = $this->createMock(RequestUtil::class); - $requestUtil->method('getCurrentPageModel')->willReturn($this->mockModelObject(PageModel::class, [ - 'layoutId' => 3, - ])); + $requestUtil->method('getCurrentPageModel')->willReturn($pageModel); + $utils = $this->createMock(Utils::class); $utils->method('request')->willReturn($requestUtil); - $layoutAdapter = $this->mockAdapter(['findByPk']); - $layoutAdapter->method('findByPk')->willReturn(null); + $configurationHelper = $this->createMock(ConfigurationHelper::class); + $configurationHelper->expects($this->once()) + ->method('isEnabledOnPage') + ->with($pageModel) + ->willReturn(false); - $framework = $this->mockContaoFramework([ - LayoutModel::class => $layoutAdapter, - ]); + $entryPointBuilderFactory = $this->createMock(EntryPointBuilderFactory::class); + $entryPointBuilderFactory->expects($this->never())->method('create'); - $templateAssetMock = $this->createMock(TemplateAsset::class); - $templateAssetMock->method('createInstance')->willReturnSelf(); - $templateAssetMock->method('linkTags')->willReturn(''); - $templateAssetMock->method('scriptTags')->willReturn('