Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Introduce FlatTemplateResolver #339

Merged
merged 1 commit into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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
22 changes: 20 additions & 2 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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:
Expand Down
146 changes: 146 additions & 0 deletions Classes/Renderer/Template/FlatTemplateResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<?php

declare(strict_types=1);

/*
* This file is part of the TYPO3 CMS extension "handlebars".
*
* Copyright (C) 2024 Elias Häußler <e.haeussler@familie-redlich.de>
*
* 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 <https://www.gnu.org/licenses/>.
*/

namespace Fr\Typo3Handlebars\Renderer\Template;

use Fr\Typo3Handlebars\Exception;
use Symfony\Component\Finder;

/**
* FlatTemplateResolver
*
* @author Elias Häußler <e.haeussler@familie-redlich.de>
* @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<string, Finder\SplFileInfo>
*/
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<string>
*/
protected function buildExtensionPatterns(): \Generator
{
foreach ($this->supportedFileExtensions as $extension) {
yield sprintf('*.%s', $extension);
}
}
}
22 changes: 19 additions & 3 deletions Documentation/Contributing/Index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions Tests/Functional/Fixtures/test_extension/composer.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
Loading
Loading