diff --git a/.gitattributes b/.gitattributes
index 30d509a..214989f 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -10,7 +10,8 @@
/docker-compose.yml export-ignore
/packaging_exclude.php export-ignore
/phpstan.neon export-ignore
-/phpunit.xml export-ignore
+/phpunit.functional.xml export-ignore
+/phpunit.unit.xml export-ignore
/rector.php export-ignore
/renovate.json export-ignore
/typoscript-lint.yml export-ignore
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 68d0035..dba5f48 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -19,6 +19,11 @@ jobs:
typo3-version: ["12.4"]
php-version: ["8.1", "8.2", "8.3"]
dependencies: ["highest", "lowest"]
+ env:
+ typo3DatabaseName: typo3
+ typo3DatabaseHost: '127.0.0.1'
+ typo3DatabaseUsername: root
+ typo3DatabasePassword: root
steps:
- uses: actions/checkout@v4
with:
@@ -32,6 +37,10 @@ jobs:
tools: composer:v2
coverage: none
+ # Start MySQL service
+ - name: Start MySQL
+ run: sudo /etc/init.d/mysql start
+
# Install dependencies
- name: Install Composer dependencies
uses: ramsey/composer-install@v2
@@ -46,6 +55,11 @@ jobs:
coverage:
name: Test coverage
runs-on: ubuntu-latest
+ env:
+ typo3DatabaseName: typo3
+ typo3DatabaseHost: '127.0.0.1'
+ typo3DatabaseUsername: root
+ typo3DatabasePassword: root
steps:
- uses: actions/checkout@v4
with:
@@ -59,6 +73,10 @@ jobs:
tools: composer:v2
coverage: pcov
+ # Start MySQL service
+ - name: Start MySQL
+ run: sudo /etc/init.d/mysql start
+
# Install dependencies
- name: Install Composer dependencies
uses: ramsey/composer-install@v2
@@ -69,13 +87,13 @@ jobs:
# Upload artifact
- name: Fix coverage path
- working-directory: .Build/log/coverage
+ working-directory: .Build/coverage
run: sed -i 's#/home/runner/work/handlebars/handlebars#${{ github.workspace }}#g' clover.xml
- name: Upload coverage artifact
uses: actions/upload-artifact@v4
with:
name: coverage
- path: .Build/log/coverage/clover.xml
+ path: .Build/coverage/clover.xml
retention-days: 7
coverage-report:
diff --git a/Classes/Renderer/Template/FlatTemplateResolver.php b/Classes/Renderer/Template/FlatTemplateResolver.php
new file mode 100644
index 0000000..bee756a
--- /dev/null
+++ b/Classes/Renderer/Template/FlatTemplateResolver.php
@@ -0,0 +1,146 @@
+
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+namespace Fr\Typo3Handlebars\Renderer\Template;
+
+use Fr\Typo3Handlebars\Exception;
+use Symfony\Component\Finder;
+
+/**
+ * FlatTemplateResolver
+ *
+ * @author Elias Häußler
+ * @license GPL-2.0-or-later
+ * @see https://fractal.build/guide/core-concepts/naming.html
+ */
+class FlatTemplateResolver extends HandlebarsTemplateResolver
+{
+ protected const VARIANT_SEPARATOR = '--';
+
+ /**
+ * @var array
+ */
+ protected array $flattenedTemplates = [];
+ protected int $depth = 30;
+
+ public function __construct(
+ TemplatePaths $templateRootPaths,
+ array $supportedFileExtensions = self::DEFAULT_FILE_EXTENSIONS,
+ ) {
+ parent::__construct($templateRootPaths, $supportedFileExtensions);
+ $this->buildTemplateMap();
+ }
+
+ public function resolveTemplatePath(string $templatePath): string
+ {
+ // Use default path resolving if path is not prefixed by "@"
+ if (!str_starts_with($templatePath, '@')) {
+ return parent::resolveTemplatePath($templatePath);
+ }
+
+ // Strip "@" prefix from given template path
+ $templateName = ltrim($templatePath, '@');
+
+ // Return filename if template exists
+ if (isset($this->flattenedTemplates[$templateName])) {
+ return $this->flattenedTemplates[$templateName]->getPathname();
+ }
+
+ // Strip off template variant
+ if (str_contains($templateName, self::VARIANT_SEPARATOR)) {
+ [$templateName] = explode(self::VARIANT_SEPARATOR, $templateName, 2);
+
+ if (isset($this->flattenedTemplates[$templateName])) {
+ return $this->flattenedTemplates[$templateName]->getPathname();
+ }
+ }
+
+ throw new Exception\TemplateNotFoundException($templateName, 1628256108);
+ }
+
+ protected function buildTemplateMap(): void
+ {
+ // Reset flattened templates
+ $this->flattenedTemplates = [];
+
+ // Instantiate finder
+ $finder = new Finder\Finder();
+ $finder->files();
+ $finder->name([...$this->buildExtensionPatterns()]);
+ $finder->depth(sprintf('< %d', $this->depth));
+
+ // Explicitly sort files and directories by name in order to streamline ordering
+ // with logic used in Fractal to ensure that the first occurrence of a flattened
+ // file is always used instead of relying on random behavior,
+ // see https://fractal.build/guide/core-concepts/naming.html#uniqueness
+ $finder->sortByName();
+
+ // Build template map
+ foreach ($this->templateRootPaths as $templateRootPath) {
+ $path = $this->resolveFilename($templateRootPath);
+ $pathFinder = clone $finder;
+ $pathFinder->in($path);
+
+ foreach ($pathFinder as $file) {
+ if ($this->isFirstOccurrenceInTemplateRoot($file)) {
+ $this->registerTemplate($file);
+ }
+ }
+ }
+ }
+
+ protected function isFirstOccurrenceInTemplateRoot(Finder\SplFileInfo $file): bool
+ {
+ $filename = $this->resolveFlatFilename($file);
+
+ // Early return if template is not registered yet
+ if (!isset($this->flattenedTemplates[$filename])) {
+ return true;
+ }
+
+ // In order to streamline template file flattening with logic used in Fractal,
+ // we always use the first flat file occurrence as resolved template, but provide
+ // the option to override exactly this file within other template root paths.
+ return $this->flattenedTemplates[$filename]->getRelativePathname() === $file->getRelativePathname();
+ }
+
+ protected function registerTemplate(Finder\SplFileInfo $file): void
+ {
+ $this->flattenedTemplates[$this->resolveFlatFilename($file)] = $file;
+ }
+
+ protected function resolveFlatFilename(Finder\SplFileInfo $file): string
+ {
+ return pathinfo($file->getPathname(), PATHINFO_FILENAME);
+ }
+
+ /**
+ * @return \Generator
+ */
+ protected function buildExtensionPatterns(): \Generator
+ {
+ foreach ($this->supportedFileExtensions as $extension) {
+ yield sprintf('*.%s', $extension);
+ }
+ }
+}
diff --git a/Documentation/Contributing/Index.rst b/Documentation/Contributing/Index.rst
index 57f1273..5ed3794 100644
--- a/Documentation/Contributing/Index.rst
+++ b/Documentation/Contributing/Index.rst
@@ -104,13 +104,29 @@ Run tests
.. code-block:: bash
- # Run tests
+ # All tests
composer test
- # Run tests with code coverage
+ # Specific tests
+ composer test:functional
+ composer test:unit
+
+ # All tests with code coverage
composer test:coverage
-The code coverage reports will be stored in :file:`.Build/log/coverage`.
+ # Specific tests with code coverage
+ composer test:coverage:functional
+ composer test:coverage:unit
+
+ # Merge code coverage of all test suites
+ composer test:coverage:merge
+
+Code coverage reports are written to :file:`.Build/coverage`. You can
+open the last merged HTML report like follows:
+
+.. code-block:: bash
+
+ open .Build/coverage/html/_merged/index.html
.. _build-documentation:
diff --git a/Tests/Functional/Fixtures/test_extension/Resources/Templates/main-layout--variant.hbs b/Tests/Functional/Fixtures/test_extension/Resources/Templates/main-layout--variant.hbs
new file mode 100644
index 0000000..033b31c
--- /dev/null
+++ b/Tests/Functional/Fixtures/test_extension/Resources/Templates/main-layout--variant.hbs
@@ -0,0 +1,25 @@
+this is the main block:
+
+{{#block "main"}}
+ main block
+{{/block}}
+
+this is the second block:
+
+{{#block "second"}}
+ second block
+{{/block}}
+
+this is the third block:
+
+{{#block "third"}}
+ third block
+{{/block}}
+
+this is the fourth block:
+
+{{#block "fourth"}}
+ fourth block
+{{/block}}
+
+this is the end. bye bye
diff --git a/Tests/Functional/Fixtures/test_extension/Resources/Templates/main-layout.hbs b/Tests/Functional/Fixtures/test_extension/Resources/Templates/main-layout.hbs
new file mode 100644
index 0000000..033b31c
--- /dev/null
+++ b/Tests/Functional/Fixtures/test_extension/Resources/Templates/main-layout.hbs
@@ -0,0 +1,25 @@
+this is the main block:
+
+{{#block "main"}}
+ main block
+{{/block}}
+
+this is the second block:
+
+{{#block "second"}}
+ second block
+{{/block}}
+
+this is the third block:
+
+{{#block "third"}}
+ third block
+{{/block}}
+
+this is the fourth block:
+
+{{#block "fourth"}}
+ fourth block
+{{/block}}
+
+this is the end. bye bye
diff --git a/Tests/Functional/Fixtures/test_extension/composer.json b/Tests/Functional/Fixtures/test_extension/composer.json
new file mode 100644
index 0000000..500ddc3
--- /dev/null
+++ b/Tests/Functional/Fixtures/test_extension/composer.json
@@ -0,0 +1,20 @@
+{
+ "name": "cpsit/typo3-handlebars-test-extension",
+ "description": "Test extension for EXT:handlebars",
+ "license": "GPL-2.0-or-later",
+ "type": "typo3-cms-extension",
+ "version": "1.0.0",
+ "require": {
+ "typo3/cms-core": "*"
+ },
+ "autoload": {
+ "psr-4": {
+ "Fr\\Typo3Handlebars\\TestExtension\\": "Classes/"
+ }
+ },
+ "extra": {
+ "typo3/cms": {
+ "extension-key": "test_extension"
+ }
+ }
+}
diff --git a/Tests/Functional/Renderer/Template/FlatTemplateResolverTest.php b/Tests/Functional/Renderer/Template/FlatTemplateResolverTest.php
new file mode 100644
index 0000000..46db707
--- /dev/null
+++ b/Tests/Functional/Renderer/Template/FlatTemplateResolverTest.php
@@ -0,0 +1,75 @@
+
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+namespace Fr\Typo3Handlebars\Tests\Functional\Renderer\Template;
+
+use Fr\Typo3Handlebars as Src;
+use Fr\Typo3Handlebars\Tests;
+use PHPUnit\Framework;
+use TYPO3\TestingFramework;
+
+/**
+ * FlatTemplateResolverTest
+ *
+ * @author Elias Häußler
+ * @license GPL-2.0-or-later
+ */
+#[Framework\Attributes\CoversClass(Src\Renderer\Template\FlatTemplateResolver::class)]
+final class FlatTemplateResolverTest extends TestingFramework\Core\Functional\FunctionalTestCase
+{
+ use Tests\Unit\HandlebarsTemplateResolverTrait;
+
+ protected array $testExtensionsToLoad = [
+ 'test_extension',
+ ];
+
+ protected bool $initializeDatabase = false;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ $this->templateResolver = new Src\Renderer\Template\FlatTemplateResolver($this->getTemplatePaths());
+ }
+
+ #[Framework\Attributes\Test]
+ public function resolveTemplatePathRespectsTemplateVariant(): void
+ {
+ $expected = $this->instancePath . '/typo3conf/ext/test_extension/Resources/Templates/main-layout--variant.hbs';
+
+ self::assertSame($expected, $this->templateResolver->resolveTemplatePath('@main-layout--variant'));
+ }
+
+ #[Framework\Attributes\Test]
+ public function resolveTemplatePathReturnsBaseTemplateForNonExistingTemplateVariant(): void
+ {
+ $expected = $this->instancePath . '/typo3conf/ext/test_extension/Resources/Templates/main-layout.hbs';
+
+ self::assertSame($expected, $this->templateResolver->resolveTemplatePath('@main-layout--non-existing-variant'));
+ }
+
+ public function getTemplateRootPath(): string
+ {
+ return 'EXT:test_extension/Resources/Templates/';
+ }
+}
diff --git a/Tests/Unit/HandlebarsTemplateResolverTrait.php b/Tests/Unit/HandlebarsTemplateResolverTrait.php
index 208f7b9..18cfd5f 100644
--- a/Tests/Unit/HandlebarsTemplateResolverTrait.php
+++ b/Tests/Unit/HandlebarsTemplateResolverTrait.php
@@ -28,7 +28,8 @@
use Fr\Typo3Handlebars\Renderer\Template\TemplateResolverInterface;
use Fr\Typo3Handlebars\Tests\Unit\Fixtures\Classes\DummyConfigurationManager;
use Fr\Typo3Handlebars\Tests\Unit\Fixtures\Classes\Renderer\Template\DummyTemplatePaths;
-use Symfony\Component\DependencyInjection\Container;
+use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
+use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
/**
* HandlebarsTemplateResolverTrait
@@ -76,21 +77,16 @@ protected function getPartialResolver(): TemplateResolverInterface
protected function getTemplatePaths(string $type = TemplatePaths::TEMPLATES): DummyTemplatePaths
{
- $container = $this->getContainer($type);
-
- $templatePaths = new DummyTemplatePaths(new DummyConfigurationManager(), $type);
- $templatePaths->setContainer($container);
-
- return $templatePaths;
+ return new DummyTemplatePaths(new DummyConfigurationManager(), $this->getParameterBag($type), $type);
}
- protected function getContainer(string $type = TemplatePaths::TEMPLATES): Container
+ protected function getParameterBag(string $type = TemplatePaths::TEMPLATES): ParameterBagInterface
{
$templateRootPath = $type === TemplatePaths::PARTIALS ? $this->getPartialRootPath() : $this->getTemplateRootPath();
- $container = new Container();
- $container->setParameter('handlebars.' . $type, [10 => $templateRootPath]);
- return $container;
+ return new ParameterBag([
+ 'handlebars.' . $type => [10 => $templateRootPath],
+ ]);
}
public function getTemplateRootPath(): string
diff --git a/Tests/Unit/Renderer/Template/TemplatePathsTest.php b/Tests/Unit/Renderer/Template/TemplatePathsTest.php
index b7a9220..bd60676 100644
--- a/Tests/Unit/Renderer/Template/TemplatePathsTest.php
+++ b/Tests/Unit/Renderer/Template/TemplatePathsTest.php
@@ -52,8 +52,7 @@ protected function setUp(): void
{
parent::setUp();
$this->configurationManager = new DummyConfigurationManager();
- $this->subject = new TemplatePaths($this->configurationManager);
- $this->subject->setContainer($this->getContainer());
+ $this->subject = new TemplatePaths($this->configurationManager, $this->getParameterBag());
}
/**
diff --git a/composer.json b/composer.json
index 7a2866e..0bc4f8e 100644
--- a/composer.json
+++ b/composer.json
@@ -24,6 +24,7 @@
"psr/log": "^1.1 || ^2.0 || ^3.0",
"symfony/config": "^6.4 || ^7.0",
"symfony/dependency-injection": "^6.4 || ^7.0",
+ "symfony/finder": "^6.4 || ^7.0",
"typo3/cms-core": "^12.4",
"typo3/cms-extbase": "^12.4",
"typo3/cms-fluid": "^12.4",
@@ -33,6 +34,7 @@
},
"require-dev": {
"armin/editorconfig-cli": "^1.8 || ^2.0",
+ "cpsit/typo3-handlebars-test-extension": "1.0.0",
"ergebnis/composer-normalize": "^2.15",
"friendsofphp/php-cs-fixer": "^3.57",
"helmich/typo3-typoscript-lint": "^2.5 || ^3.0",
@@ -40,6 +42,7 @@
"phpstan/extension-installer": "^1.3",
"phpstan/phpstan": "^1.9",
"phpstan/phpstan-phpunit": "^1.1",
+ "phpunit/phpcov": "^9.0 || ^10.0",
"phpunit/phpunit": "^10.1 || ^11.0",
"saschaegerer/phpstan-typo3": "^1.0",
"ssch/typo3-rector": "^2.0",
@@ -47,6 +50,12 @@
"typo3/coding-standards": "^0.8.0",
"typo3/testing-framework": "^8.0"
},
+ "repositories": [
+ {
+ "type": "path",
+ "url": "Tests/Functional/Fixtures/*"
+ }
+ ],
"autoload": {
"psr-4": {
"Fr\\Typo3Handlebars\\": "Classes/"
@@ -113,7 +122,19 @@
"@sca:php"
],
"sca:php": "phpstan analyse -c phpstan.neon",
- "test": "@test:coverage --no-coverage",
- "test:coverage": "phpunit -c phpunit.xml"
+ "test": [
+ "@test:functional",
+ "@test:unit"
+ ],
+ "test:coverage": [
+ "@test:coverage:functional",
+ "@test:coverage:unit",
+ "@test:coverage:merge"
+ ],
+ "test:coverage:functional": "phpunit -c phpunit.functional.xml",
+ "test:coverage:merge": "phpcov merge --html .Build/coverage/html/_merged --clover .Build/coverage/clover.xml --text php://stdout .Build/coverage/php",
+ "test:coverage:unit": "phpunit -c phpunit.unit.xml",
+ "test:functional": "@test:coverage:functional --no-coverage",
+ "test:unit": "@test:coverage:unit --no-coverage"
}
}
diff --git a/composer.lock b/composer.lock
index 4b398a9..5c9699e 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "5d04f6d9496fac18b1ad38752eed8e8a",
+ "content-hash": "c41bb03f6dab0a8653d8ae9d7a1050d8",
"packages": [
{
"name": "bacon/bacon-qr-code",
@@ -5935,6 +5935,36 @@
],
"time": "2024-05-06T16:37:16+00:00"
},
+ {
+ "name": "cpsit/typo3-handlebars-test-extension",
+ "version": "1.0.0",
+ "dist": {
+ "type": "path",
+ "url": "Tests/Functional/Fixtures/test_extension",
+ "reference": "df86f70cfd3060828c241882cafe72cf35b25c9f"
+ },
+ "require": {
+ "typo3/cms-core": "*"
+ },
+ "type": "typo3-cms-extension",
+ "extra": {
+ "typo3/cms": {
+ "extension-key": "test_extension"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Fr\\Typo3Handlebars\\TestExtension\\": "Classes/"
+ }
+ },
+ "license": [
+ "GPL-2.0-or-later"
+ ],
+ "description": "Test extension for EXT:handlebars",
+ "transport-options": {
+ "relative": true
+ }
+ },
{
"name": "ergebnis/composer-normalize",
"version": "2.43.0",
@@ -7948,6 +7978,68 @@
],
"time": "2023-02-03T06:57:52+00:00"
},
+ {
+ "name": "phpunit/phpcov",
+ "version": "9.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpcov.git",
+ "reference": "05307478b8f4b2a50c508d6f4eca15704cf7c1fd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpcov/zipball/05307478b8f4b2a50c508d6f4eca15704cf7c1fd",
+ "reference": "05307478b8f4b2a50c508d6f4eca15704cf7c1fd",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "phpunit/php-code-coverage": "^10.1.5",
+ "phpunit/php-file-iterator": "^4.0",
+ "phpunit/phpunit": "^10.0",
+ "sebastian/cli-parser": "^2.0",
+ "sebastian/diff": "^5.0",
+ "sebastian/version": "^4.0"
+ },
+ "bin": [
+ "phpcov"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "9.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "CLI frontend for php-code-coverage",
+ "homepage": "https://github.com/sebastianbergmann/phpcov",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/phpcov/issues",
+ "source": "https://github.com/sebastianbergmann/phpcov/tree/9.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-09-12T14:47:35+00:00"
+ },
{
"name": "phpunit/phpunit",
"version": "10.5.34",
diff --git a/packaging_exclude.php b/packaging_exclude.php
index f4fc9b8..b2eef6c 100644
--- a/packaging_exclude.php
+++ b/packaging_exclude.php
@@ -41,7 +41,8 @@
'packaging_exclude.php',
'php-cs-fixer.php',
'phpstan.neon',
- 'phpunit.xml',
+ 'phpunit.functional.xml',
+ 'phpunit.unit.xml',
'rector.php',
'renovate.json',
'typoscript-lint.yml',
diff --git a/phpunit.functional.xml b/phpunit.functional.xml
new file mode 100644
index 0000000..ad99628
--- /dev/null
+++ b/phpunit.functional.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+ Tests/Functional
+
+
+
+
+
+
+
diff --git a/phpunit.xml b/phpunit.unit.xml
similarity index 72%
rename from phpunit.xml
rename to phpunit.unit.xml
index bc6d10f..5c956db 100644
--- a/phpunit.xml
+++ b/phpunit.unit.xml
@@ -7,9 +7,9 @@
>
-
-
-
+
+
+
@@ -18,7 +18,7 @@
-
+