diff --git a/Classes/DependencyInjection/HandlebarsHelperPass.php b/Classes/DependencyInjection/HandlebarsHelperPass.php index 6e7aa8dc..2700d161 100644 --- a/Classes/DependencyInjection/HandlebarsHelperPass.php +++ b/Classes/DependencyInjection/HandlebarsHelperPass.php @@ -23,11 +23,8 @@ namespace Fr\Typo3Handlebars\DependencyInjection; -use Fr\Typo3Handlebars\Renderer\HelperAwareInterface; -use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Definition; -use Symfony\Component\DependencyInjection\Reference; +use Fr\Typo3Handlebars\Renderer; +use Symfony\Component\DependencyInjection; /** * HandlebarsHelperPass @@ -37,56 +34,28 @@ * @internal * @codeCoverageIgnore */ -final class HandlebarsHelperPass implements CompilerPassInterface +final readonly class HandlebarsHelperPass implements DependencyInjection\Compiler\CompilerPassInterface { - /** - * @var Definition[] - */ - private array $rendererDefinitions = []; - public function __construct( - private readonly string $helperTagName, - private readonly string $rendererTagName, + private string $helperTagName, ) {} - public function process(ContainerBuilder $container): void + public function process(DependencyInjection\ContainerBuilder $container): void { - $this->fetchRendererDefinitions($container); + $registryDefinition = $container->getDefinition(Renderer\Helper\HelperRegistry::class); // Register tagged Handlebars helper at all Helper-aware renderers foreach ($container->findTaggedServiceIds($this->helperTagName) as $serviceId => $tags) { - $container->findDefinition($serviceId)->setPublic(true); - foreach (array_filter($tags) as $attributes) { $this->validateTag($serviceId, $attributes); - $this->registerHelper( - $attributes['identifier'], - [new Reference($serviceId), $attributes['method']] - ); - } - } - } - - /** - * @param array{0: string|Reference, 1: string} $callable - */ - private function registerHelper(string $name, array $callable): void - { - foreach ($this->rendererDefinitions as $rendererDefinition) { - $rendererDefinition->addMethodCall('registerHelper', [$name, $callable]); - } - } - protected function fetchRendererDefinitions(ContainerBuilder $container): void - { - $this->rendererDefinitions = []; - - foreach (array_keys($container->findTaggedServiceIds($this->rendererTagName)) as $serviceId) { - $rendererDefinition = $container->findDefinition($serviceId); - $rendererClass = $rendererDefinition->getClass(); - - if ($rendererClass !== null && \in_array(HelperAwareInterface::class, class_implements($rendererClass) ?: [])) { - $this->rendererDefinitions[] = $rendererDefinition; + $registryDefinition->addMethodCall( + 'add', + [ + $attributes['identifier'], + [new DependencyInjection\Reference($serviceId), $attributes['method']], + ], + ); } } } @@ -99,13 +68,13 @@ private function validateTag(string $serviceId, array $tagAttributes): void if (!\array_key_exists('identifier', $tagAttributes) || (string)$tagAttributes['identifier'] === '') { throw new \InvalidArgumentException( \sprintf('Service tag "%s" requires an identifier attribute to be defined, missing in: %s', $this->helperTagName, $serviceId), - 1606236820 + 1606236820, ); } if (!\array_key_exists('method', $tagAttributes) || (string)$tagAttributes['method'] === '') { throw new \InvalidArgumentException( \sprintf('Service tag "%s" requires an method attribute to be defined, missing in: %s', $this->helperTagName, $serviceId), - 1606245140 + 1606245140, ); } } diff --git a/Classes/Exception/Exception.php b/Classes/Exception/Exception.php new file mode 100644 index 00000000..a809badd --- /dev/null +++ b/Classes/Exception/Exception.php @@ -0,0 +1,32 @@ + + * + * 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\Exception; + +/** + * Exception + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +abstract class Exception extends \Exception {} diff --git a/Classes/Renderer/HelperAwareInterface.php b/Classes/Exception/HelperIsNotRegistered.php similarity index 65% rename from Classes/Renderer/HelperAwareInterface.php rename to Classes/Exception/HelperIsNotRegistered.php index 25f16ae4..77126eef 100644 --- a/Classes/Renderer/HelperAwareInterface.php +++ b/Classes/Exception/HelperIsNotRegistered.php @@ -5,7 +5,7 @@ /* * This file is part of the TYPO3 CMS extension "handlebars". * - * Copyright (C) 2021 Elias Häußler + * Copyright (C) 2025 Elias Häußler * * 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 @@ -21,25 +21,21 @@ * along with this program. If not, see . */ -namespace Fr\Typo3Handlebars\Renderer; +namespace Fr\Typo3Handlebars\Exception; /** - * HelperAwareInterface + * HelperIsNotRegistered * * @author Elias Häußler * @license GPL-2.0-or-later */ -interface HelperAwareInterface +final class HelperIsNotRegistered extends Exception { - /** - * Get all registered Handlebars helpers. - * - * @return array - */ - public function getHelpers(): array; - - /** - * Register new Handlebars helper with given function. - */ - public function registerHelper(string $name, mixed $function): void; + public function __construct(string $name) + { + parent::__construct( + \sprintf('Handlebars helper "%s" is not registered.', $name), + 1736242470, + ); + } } diff --git a/Classes/Renderer/HandlebarsRenderer.php b/Classes/Renderer/HandlebarsRenderer.php index 58fd49dd..e0a9e287 100644 --- a/Classes/Renderer/HandlebarsRenderer.php +++ b/Classes/Renderer/HandlebarsRenderer.php @@ -30,8 +30,8 @@ use Fr\Typo3Handlebars\Exception\InvalidTemplateFileException; use Fr\Typo3Handlebars\Exception\TemplateCompilationException; use Fr\Typo3Handlebars\Exception\TemplateNotFoundException; +use Fr\Typo3Handlebars\Renderer\Helper\HelperRegistry; use Fr\Typo3Handlebars\Renderer\Template\TemplateResolverInterface; -use Fr\Typo3Handlebars\Traits\HandlebarsHelperTrait; use LightnCandy\Context; use LightnCandy\LightnCandy; use LightnCandy\Partial; @@ -52,10 +52,8 @@ */ #[AsAlias('handlebars.renderer')] #[Autoconfigure(tags: ['handlebars.renderer'])] -class HandlebarsRenderer implements RendererInterface, HelperAwareInterface +class HandlebarsRenderer implements RendererInterface { - use HandlebarsHelperTrait; - protected readonly bool $debugMode; /** @@ -65,6 +63,7 @@ public function __construct( #[Autowire('@handlebars.cache')] protected readonly CacheInterface $cache, protected readonly EventDispatcherInterface $eventDispatcher, + protected readonly HelperRegistry $helperRegistry, protected readonly LoggerInterface $logger, #[Autowire('@handlebars.template_resolver')] protected readonly TemplateResolverInterface $templateResolver, @@ -122,7 +121,7 @@ protected function processRendering(string $templatePath, array $data): string // Render content $content = $renderer($beforeRenderingEvent->getData(), [ 'debug' => Runtime::DEBUG_TAGS_HTML, - 'helpers' => $this->helpers, + 'helpers' => $this->helperRegistry->getAll(), ]); // Dispatch after rendering event @@ -235,7 +234,7 @@ protected function prepareCompileResult(string $compileResult): callable */ protected function getHelperStubs(): array { - return array_fill_keys(array_keys($this->helpers), true); + return array_fill_keys(array_keys($this->helperRegistry->getAll()), true); } /** diff --git a/Classes/Traits/HandlebarsHelperTrait.php b/Classes/Renderer/Helper/HelperRegistry.php similarity index 66% rename from Classes/Traits/HandlebarsHelperTrait.php rename to Classes/Renderer/Helper/HelperRegistry.php index 9573e962..15743264 100644 --- a/Classes/Traits/HandlebarsHelperTrait.php +++ b/Classes/Renderer/Helper/HelperRegistry.php @@ -5,7 +5,7 @@ /* * This file is part of the TYPO3 CMS extension "handlebars". * - * Copyright (C) 2020 Elias Häußler + * Copyright (C) 2025 Elias Häußler * * 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 @@ -21,26 +21,30 @@ * along with this program. If not, see . */ -namespace Fr\Typo3Handlebars\Traits; +namespace Fr\Typo3Handlebars\Renderer\Helper; use Fr\Typo3Handlebars\Exception; -use Fr\Typo3Handlebars\Renderer; +use Psr\Log; use TYPO3\CMS\Core; /** - * HandlebarsHelperTrait + * HelperRegistry * * @author Elias Häußler * @license GPL-2.0-or-later */ -trait HandlebarsHelperTrait +final class HelperRegistry implements Core\SingletonInterface { /** - * @var array + * @var array */ - protected array $helpers = []; + private array $helpers = []; - public function registerHelper(string $name, mixed $function): void + public function __construct( + private readonly Log\LoggerInterface $logger, + ) {} + + public function add(string $name, mixed $function): void { try { $this->helpers[$name] = $this->decorateHelperFunction( @@ -59,18 +63,35 @@ public function registerHelper(string $name, mixed $function): void } /** - * @return array + * @throws Exception\HelperIsNotRegistered + */ + public function get(string $name): callable + { + if (!isset($this->helpers[$name])) { + throw new Exception\HelperIsNotRegistered($name); + } + + return $this->helpers[$name]; + } + + /** + * @return array */ - public function getHelpers(): array + public function getAll(): array { return $this->helpers; } + public function has(string $name): bool + { + return isset($this->helpers[$name]); + } + /** * @throws Exception\InvalidHelperException * @throws \ReflectionException */ - protected function resolveHelperFunction(mixed $function): callable + private function resolveHelperFunction(mixed $function): callable { // Try to resolve the Helper function in this order: // @@ -78,8 +99,7 @@ protected function resolveHelperFunction(mixed $function): callable // ├─ a. as string // └─ b. as closure or first class callable syntax // 2. invokable class - // ├─ a. as string (class-name) - // └─ b. as object + // └─ a. as string (class-name) // 3. class implementing Helper interface // ├─ a. as string (class-name) // └─ b. as object @@ -103,7 +123,7 @@ protected function resolveHelperFunction(mixed $function): callable } // 3a. class implementing Helper interface as string - if (class_exists($function) && \is_a($function, Renderer\Helper\HelperInterface::class, true)) { + if (class_exists($function) && \is_a($function, HelperInterface::class, true)) { return Core\Utility\GeneralUtility::makeInstance($function)->render(...); } } @@ -113,16 +133,9 @@ protected function resolveHelperFunction(mixed $function): callable return $function; } - if (\is_object($function)) { - // 2b. invokable class as object - if (\is_callable($function)) { - return $function; - } - - // 3b. class implementing Helper interface as object - if ($function instanceof Renderer\Helper\HelperInterface) { - return $function->render(...); - } + // 3b. class implementing Helper interface as object + if (\is_object($function) && $function instanceof HelperInterface) { + return $function->render(...); } // 4a. class method as string @@ -148,12 +161,6 @@ protected function resolveHelperFunction(mixed $function): callable throw Exception\InvalidHelperException::forFunction($className . '::' . $methodName); } - // Check if method can be called statically - $callable = [$className, $methodName]; - if ($reflectionMethod->isStatic() && \is_callable($callable)) { - return $callable; - } - // Instantiate class if not done yet /** @var class-string $className */ if (\is_string($className)) { @@ -169,38 +176,15 @@ protected function resolveHelperFunction(mixed $function): callable } /** - * @return callable(\Fr\Typo3Handlebars\Renderer\Helper\Context\HelperContext): mixed + * @return callable(Context\HelperContext): mixed */ - protected function decorateHelperFunction(callable $function): callable + private function decorateHelperFunction(callable $function): callable { return static function () use ($function) { $arguments = \func_get_args(); - $context = Renderer\Helper\Context\HelperContext::fromRuntimeCall($arguments); + $context = Context\HelperContext::fromRuntimeCall($arguments); return $function($context); }; } - - /** - * @codeCoverageIgnore - * @deprecated use resolveHelperFunction() instead and check for thrown exceptions - */ - protected function isValidHelper(mixed $helperFunction): bool - { - trigger_error( - \sprintf( - 'The method "%s" is deprecated and will be removed with 0.9.0. ' . - 'Use "%s::resolveHelperFunction()" instead and check for thrown exceptions.', - __METHOD__, - __TRAIT__, - ), - E_USER_DEPRECATED, - ); - - try { - return (bool)$this->resolveHelperFunction($helperFunction); - } catch (Exception\InvalidHelperException | \ReflectionException) { - return false; - } - } } diff --git a/Configuration/Services.php b/Configuration/Services.php index 0102cfec..3d05c27c 100644 --- a/Configuration/Services.php +++ b/Configuration/Services.php @@ -33,7 +33,7 @@ return static function (ContainerConfigurator $containerConfigurator, ContainerBuilder $container): void { $container->registerExtension(new HandlebarsExtension()); $container->addCompilerPass(new DataProcessorPass('handlebars.processor', 'handlebars.compatibility_layer')); - $container->addCompilerPass(new HandlebarsHelperPass(AsHelper::TAG_NAME, 'handlebars.renderer')); + $container->addCompilerPass(new HandlebarsHelperPass(AsHelper::TAG_NAME)); $container->addCompilerPass(new FeatureRegistrationPass(), priority: 30); $container->registerAttributeForAutoconfiguration( diff --git a/Tests/Functional/Renderer/Helper/BlockHelperTest.php b/Tests/Functional/Renderer/Helper/BlockHelperTest.php index 8c971c6a..100d8f11 100644 --- a/Tests/Functional/Renderer/Helper/BlockHelperTest.php +++ b/Tests/Functional/Renderer/Helper/BlockHelperTest.php @@ -54,18 +54,22 @@ protected function setUp(): void { parent::setUp(); + $helperRegistry = new Src\Renderer\Helper\HelperRegistry(new Log\NullLogger()); + $this->templateRootPath = 'EXT:test_extension/Resources/Templates/'; $this->logger = new Log\Test\TestLogger(); $this->templateResolver = new Src\Renderer\Template\FlatTemplateResolver($this->getTemplatePaths()); $this->renderer = new Src\Renderer\HandlebarsRenderer( new Src\Cache\NullCache(), new EventDispatcher\EventDispatcher(), + $helperRegistry, $this->logger, $this->templateResolver, ); - $this->renderer->registerHelper('extend', new Src\Renderer\Helper\ExtendHelper($this->renderer)); - $this->renderer->registerHelper('content', new Src\Renderer\Helper\ContentHelper($this->logger)); - $this->renderer->registerHelper('block', new Src\Renderer\Helper\BlockHelper()); + + $helperRegistry->add('extend', new Src\Renderer\Helper\ExtendHelper($this->renderer)); + $helperRegistry->add('content', new Src\Renderer\Helper\ContentHelper($this->logger)); + $helperRegistry->add('block', new Src\Renderer\Helper\BlockHelper()); } #[Framework\Attributes\Test] diff --git a/Tests/Functional/Renderer/Helper/ContentHelperTest.php b/Tests/Functional/Renderer/Helper/ContentHelperTest.php index 24ad6e89..fb9358d0 100644 --- a/Tests/Functional/Renderer/Helper/ContentHelperTest.php +++ b/Tests/Functional/Renderer/Helper/ContentHelperTest.php @@ -54,18 +54,22 @@ protected function setUp(): void { parent::setUp(); + $helperRegistry = new Src\Renderer\Helper\HelperRegistry(new Log\NullLogger()); + $this->templateRootPath = 'EXT:test_extension/Resources/Templates/'; $this->logger = new Log\Test\TestLogger(); $this->templateResolver = new Src\Renderer\Template\FlatTemplateResolver($this->getTemplatePaths()); $this->renderer = new Src\Renderer\HandlebarsRenderer( new Src\Cache\NullCache(), new EventDispatcher\EventDispatcher(), + $helperRegistry, $this->logger, $this->templateResolver, ); - $this->renderer->registerHelper('extend', new Src\Renderer\Helper\ExtendHelper($this->renderer)); - $this->renderer->registerHelper('content', new Src\Renderer\Helper\ContentHelper($this->logger)); - $this->renderer->registerHelper('block', new Src\Renderer\Helper\BlockHelper()); + + $helperRegistry->add('extend', new Src\Renderer\Helper\ExtendHelper($this->renderer)); + $helperRegistry->add('content', new Src\Renderer\Helper\ContentHelper($this->logger)); + $helperRegistry->add('block', new Src\Renderer\Helper\BlockHelper()); } #[Framework\Attributes\Test] diff --git a/Tests/Functional/Renderer/Helper/ExtendHelperTest.php b/Tests/Functional/Renderer/Helper/ExtendHelperTest.php index b5857b9e..c955bf06 100644 --- a/Tests/Functional/Renderer/Helper/ExtendHelperTest.php +++ b/Tests/Functional/Renderer/Helper/ExtendHelperTest.php @@ -54,16 +54,20 @@ protected function setUp(): void { parent::setUp(); + $helperRegistry = new Src\Renderer\Helper\HelperRegistry(new Log\NullLogger()); + $this->templateRootPath = 'EXT:test_extension/Resources/Templates/'; $this->templateResolver = new Src\Renderer\Template\FlatTemplateResolver($this->getTemplatePaths()); $this->renderer = new Src\Renderer\HandlebarsRenderer( new Src\Cache\NullCache(), new EventDispatcher\EventDispatcher(), + $helperRegistry, new Log\NullLogger(), $this->templateResolver, ); - $this->renderer->registerHelper('extend', new Src\Renderer\Helper\ExtendHelper($this->renderer)); - $this->renderer->registerHelper('jsonEncode', new TestExtension\JsonHelper()); + + $helperRegistry->add('extend', new Src\Renderer\Helper\ExtendHelper($this->renderer)); + $helperRegistry->add('jsonEncode', new TestExtension\JsonHelper()); } #[Framework\Attributes\Test] diff --git a/Tests/Functional/Renderer/Helper/RenderHelperTest.php b/Tests/Functional/Renderer/Helper/RenderHelperTest.php index 6620713c..2f753247 100644 --- a/Tests/Functional/Renderer/Helper/RenderHelperTest.php +++ b/Tests/Functional/Renderer/Helper/RenderHelperTest.php @@ -57,11 +57,14 @@ protected function setUp(): void { parent::setUp(); + $helperRegistry = new Src\Renderer\Helper\HelperRegistry(new Log\NullLogger()); + $this->templateRootPath = 'EXT:test_extension/Resources/Templates/'; $this->templateResolver = new Src\Renderer\Template\FlatTemplateResolver($this->getTemplatePaths()); $this->renderer = new Src\Renderer\HandlebarsRenderer( new Src\Cache\NullCache(), new EventDispatcher\EventDispatcher(), + $helperRegistry, new Log\NullLogger(), $this->templateResolver, ); @@ -75,7 +78,7 @@ protected function setUp(): void $this->contentObjectRenderer, ); - $this->renderer->registerHelper('render', $subject); + $helperRegistry->add('render', $subject); } #[Framework\Attributes\Test] diff --git a/Tests/Unit/DataProcessing/AbstractDataProcessorTest.php b/Tests/Unit/DataProcessing/AbstractDataProcessorTest.php index 539666c0..d17348da 100644 --- a/Tests/Unit/DataProcessing/AbstractDataProcessorTest.php +++ b/Tests/Unit/DataProcessing/AbstractDataProcessorTest.php @@ -60,6 +60,7 @@ protected function setUp(): void new Src\Renderer\HandlebarsRenderer( $this->getCache(), new EventDispatcher\EventDispatcher(), + new Src\Renderer\Helper\HelperRegistry($this->logger), $this->logger, $this->getTemplateResolver(), ), diff --git a/Tests/Unit/DataProcessing/SimpleProcessorTest.php b/Tests/Unit/DataProcessing/SimpleProcessorTest.php index b06fc72f..dff95432 100644 --- a/Tests/Unit/DataProcessing/SimpleProcessorTest.php +++ b/Tests/Unit/DataProcessing/SimpleProcessorTest.php @@ -46,6 +46,7 @@ final class SimpleProcessorTest extends TestingFramework\Core\Unit\UnitTestCase private Frontend\ContentObject\ContentObjectRenderer&Framework\MockObject\MockObject $contentObjectRendererMock; private Log\Test\TestLogger $logger; + private Src\Renderer\Helper\HelperRegistry $helperRegistry; private Src\Renderer\HandlebarsRenderer $renderer; private Src\DataProcessing\SimpleProcessor $subject; @@ -55,9 +56,11 @@ protected function setUp(): void $this->contentObjectRendererMock = $this->createMock(Frontend\ContentObject\ContentObjectRenderer::class); $this->logger = new Log\Test\TestLogger(); + $this->helperRegistry = new Src\Renderer\Helper\HelperRegistry($this->logger); $this->renderer = new Src\Renderer\HandlebarsRenderer( $this->getCache(), new EventDispatcher\EventDispatcher(), + $this->helperRegistry, $this->logger, $this->getTemplateResolver(), ); @@ -88,7 +91,7 @@ public function processThrowsExceptionIfTemplatePathIsNotConfigured(array $confi #[Framework\Attributes\Test] public function processReturnsRenderedTemplate(): void { - $this->renderer->registerHelper('varDump', Src\Renderer\Helper\VarDumpHelper::class); + $this->helperRegistry->add('varDump', Src\Renderer\Helper\VarDumpHelper::class); $this->contentObjectRendererMock->data = [ 'uid' => 1, diff --git a/Tests/Unit/Exception/HelperIsNotRegisteredTest.php b/Tests/Unit/Exception/HelperIsNotRegisteredTest.php new file mode 100644 index 00000000..3651a5f6 --- /dev/null +++ b/Tests/Unit/Exception/HelperIsNotRegisteredTest.php @@ -0,0 +1,47 @@ + + * + * 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\Unit\Exception; + +use Fr\Typo3Handlebars as Src; +use PHPUnit\Framework; +use TYPO3\TestingFramework; + +/** + * HelperIsNotRegisteredTest + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +#[Framework\Attributes\CoversClass(Src\Exception\HelperIsNotRegistered::class)] +final class HelperIsNotRegisteredTest extends TestingFramework\Core\Unit\UnitTestCase +{ + #[Framework\Attributes\Test] + public function constructorReturnsExceptionForUnregisteredHelper(): void + { + $actual = new Src\Exception\HelperIsNotRegistered('foo'); + + self::assertSame('Handlebars helper "foo" is not registered.', $actual->getMessage()); + self::assertSame(1736242470, $actual->getCode()); + } +} diff --git a/Tests/Unit/Fixtures/Classes/Renderer/Helper/DummyHelper.php b/Tests/Unit/Fixtures/Classes/Renderer/Helper/DummyHelper.php index 2ac29b35..b9455300 100644 --- a/Tests/Unit/Fixtures/Classes/Renderer/Helper/DummyHelper.php +++ b/Tests/Unit/Fixtures/Classes/Renderer/Helper/DummyHelper.php @@ -39,11 +39,6 @@ public function render(Renderer\Helper\Context\HelperContext $context): string return 'foo'; } - public function __invoke(): string - { - return 'foo'; - } - public static function staticExecute(): string { return 'foo'; diff --git a/Tests/Unit/Fixtures/Classes/Traits/DummyHandlebarsHelperTraitClass.php b/Tests/Unit/Fixtures/Classes/Renderer/Helper/DummyInvokableHelper.php similarity index 74% rename from Tests/Unit/Fixtures/Classes/Traits/DummyHandlebarsHelperTraitClass.php rename to Tests/Unit/Fixtures/Classes/Renderer/Helper/DummyInvokableHelper.php index 3da75ec6..5041d1c0 100644 --- a/Tests/Unit/Fixtures/Classes/Traits/DummyHandlebarsHelperTraitClass.php +++ b/Tests/Unit/Fixtures/Classes/Renderer/Helper/DummyInvokableHelper.php @@ -21,23 +21,19 @@ * along with this program. If not, see . */ -namespace Fr\Typo3Handlebars\Tests\Unit\Fixtures\Classes\Traits; - -use Fr\Typo3Handlebars\Traits; -use Psr\Log; +namespace Fr\Typo3Handlebars\Tests\Unit\Fixtures\Classes\Renderer\Helper; /** - * DummyHandlebarsHelperTraitClass + * DummyInvokableHelper * * @author Elias Häußler * @license GPL-2.0-or-later * @internal */ -final class DummyHandlebarsHelperTraitClass +final readonly class DummyInvokableHelper { - use Traits\HandlebarsHelperTrait; - - public function __construct( - private readonly Log\LoggerInterface $logger, - ) {} + public function __invoke(): string + { + return 'foo'; + } } diff --git a/Tests/Unit/Renderer/HandlebarsRendererTest.php b/Tests/Unit/Renderer/HandlebarsRendererTest.php index dbe0629d..0b083e38 100644 --- a/Tests/Unit/Renderer/HandlebarsRendererTest.php +++ b/Tests/Unit/Renderer/HandlebarsRendererTest.php @@ -45,6 +45,7 @@ final class HandlebarsRendererTest extends TestingFramework\Core\Unit\UnitTestCa use Tests\HandlebarsTemplateResolverTrait; private Log\Test\TestLogger $logger; + private Src\Renderer\Helper\HelperRegistry $helperRegistry; private Src\Renderer\HandlebarsRenderer $subject; private Frontend\Controller\TypoScriptFrontendController&Framework\MockObject\MockObject $tsfeMock; @@ -72,10 +73,7 @@ public function renderLogsCriticalErrorIfGivenTemplateIsNotAvailable(): void })); } - /** - * @test - * @ - */ + #[Framework\Attributes\Test] public function renderLogsCriticalErrorIfGivenTemplateIsNotReadable(): void { $this->templateResolver = new Tests\Unit\Fixtures\Classes\Renderer\Template\DummyTemplateResolver(); @@ -106,7 +104,7 @@ public function renderReturnsEmptyStringIfGivenTemplateIsEmpty(): void #[Framework\Attributes\Test] public function renderMergesDefaultDataWithGivenData(): void { - $this->subject->registerHelper('varDump', Src\Renderer\Helper\VarDumpHelper::class); + $this->helperRegistry->add('varDump', Src\Renderer\Helper\VarDumpHelper::class); $this->subject->setDefaultData([ 'foo' => 'baz', ]); @@ -229,6 +227,7 @@ public function resolvePartialReturnsNullIfNoPartialResolverIsRegistered(): void $subject = new Src\Renderer\HandlebarsRenderer( $this->getCache(), new EventDispatcher\EventDispatcher(), + $this->helperRegistry, $this->logger, $this->getTemplateResolver(), ); @@ -287,10 +286,12 @@ protected function tearDown(): void private function renewSubject(string $rendererClass = Src\Renderer\HandlebarsRenderer::class): Src\Renderer\HandlebarsRenderer { $this->logger = new Log\Test\TestLogger(); + $this->helperRegistry = new Src\Renderer\Helper\HelperRegistry($this->logger); return $this->subject = new $rendererClass( $this->getCache(), new EventDispatcher\EventDispatcher(), + $this->helperRegistry, $this->logger, $this->getTemplateResolver(), $this->getPartialResolver(), diff --git a/Tests/Unit/Traits/HandlebarsHelperTraitTest.php b/Tests/Unit/Renderer/Helper/HelperRegistryTest.php similarity index 51% rename from Tests/Unit/Traits/HandlebarsHelperTraitTest.php rename to Tests/Unit/Renderer/Helper/HelperRegistryTest.php index fac608f8..9b9afe54 100644 --- a/Tests/Unit/Traits/HandlebarsHelperTraitTest.php +++ b/Tests/Unit/Renderer/Helper/HelperRegistryTest.php @@ -21,7 +21,7 @@ * along with this program. If not, see . */ -namespace Fr\Typo3Handlebars\Tests\Unit\Traits; +namespace Fr\Typo3Handlebars\Tests\Unit\Renderer\Helper; use Fr\Typo3Handlebars as Src; use Fr\Typo3Handlebars\Tests; @@ -29,74 +29,127 @@ use Psr\Log; use TYPO3\TestingFramework; +use function trim; + /** - * HandlebarsHelperTraitTest + * HelperRegistryTest * * @author Elias Häußler * @license GPL-2.0-or-later */ -#[Framework\Attributes\CoversClass(Src\Traits\HandlebarsHelperTrait::class)] -final class HandlebarsHelperTraitTest extends TestingFramework\Core\Unit\UnitTestCase +#[Framework\Attributes\CoversClass(Src\Renderer\Helper\HelperRegistry::class)] +final class HelperRegistryTest extends TestingFramework\Core\Unit\UnitTestCase { private Log\Test\TestLogger $logger; - private Tests\Unit\Fixtures\Classes\Traits\DummyHandlebarsHelperTraitClass $subject; + private Src\Renderer\Helper\HelperRegistry $subject; protected function setUp(): void { parent::setUp(); $this->logger = new Log\Test\TestLogger(); - $this->subject = new Tests\Unit\Fixtures\Classes\Traits\DummyHandlebarsHelperTraitClass($this->logger); + $this->subject = new Src\Renderer\Helper\HelperRegistry($this->logger); } #[Framework\Attributes\Test] - #[Framework\Attributes\DataProvider('registerHelperLogsCriticalErrorIfGivenHelperIsInvalidDataProvider')] - public function registerHelperLogsCriticalErrorIfGivenHelperIsInvalid(mixed $function): void + #[Framework\Attributes\DataProvider('addLogsCriticalErrorIfGivenHelperIsInvalidDataProvider')] + public function addLogsCriticalErrorIfGivenHelperIsInvalid(mixed $function): void { - $this->subject->registerHelper('foo', $function); + $this->subject->add('foo', $function); + self::assertTrue($this->logger->hasCriticalThatPasses(function ($logRecord) use ($function) { self::assertSame('Error while registering Handlebars helper "foo".', $logRecord['message']); self::assertSame('foo', $logRecord['context']['name']); self::assertSame($function, $logRecord['context']['function']); return true; })); - self::assertSame([], $this->subject->getHelpers()); + self::assertSame([], $this->subject->getAll()); } #[Framework\Attributes\Test] - #[Framework\Attributes\DataProvider('registerHelperRegistersHelperCorrectlyDataProvider')] - public function registerHelperRegistersHelperCorrectly(mixed $function, callable $expectedCallable): void + #[Framework\Attributes\DataProvider('addRegistersHelperCorrectlyDataProvider')] + public function addRegistersHelperCorrectly(mixed $function, callable $expectedCallable): void { - $this->subject->registerHelper('foo', $function); + $this->subject->add('foo', $function); $expected = $this->mapExpectedCallable($expectedCallable); - self::assertEquals(['foo' => $expected], $this->subject->getHelpers()); + self::assertEquals($expected, $this->subject->get('foo')); + } + + #[Framework\Attributes\Test] + public function addDecoratesHelperFunction(): void + { + $function = static fn(Src\Renderer\Helper\Context\HelperContext $context) => $context[0] . $context['foo']; + + $options = [ + 'hash' => [ + 'foo' => 'baz', + ], + 'contexts' => [null], + '_this' => [], + 'data' => [], + ]; + + $this->subject->add('foo', $function); + + self::assertSame('foobaz', $this->subject->get('foo')('foo', $options)); + } + + #[Framework\Attributes\Test] + public function addOverridesAvailableHelper(): void + { + $this->subject->add('foo', 'trim'); + + self::assertEquals($this->mapExpectedCallable(trim(...)), $this->subject->get('foo')); + + $this->subject->add('foo', 'strtolower'); + + self::assertEquals($this->mapExpectedCallable(strtolower(...)), $this->subject->get('foo')); + } + + #[Framework\Attributes\Test] + public function getThrowsExceptionIfGivenHelperIsNotRegistered(): void + { + $this->expectExceptionObject( + new Src\Exception\HelperIsNotRegistered('foo'), + ); + + $this->subject->get('foo'); } #[Framework\Attributes\Test] - public function registerHelperOverridesAvailableHelper(): void + public function getReturnsRegisteredHelper(): void { - $this->subject->registerHelper('foo', 'trim'); - self::assertEquals(['foo' => $this->mapExpectedCallable(trim(...))], $this->subject->getHelpers()); + $this->subject->add('foo', 'trim'); + + self::assertEquals($this->mapExpectedCallable(trim(...)), $this->subject->get('foo')); + } + + #[Framework\Attributes\Test] + public function getAllReturnsRegisteredHelpers(): void + { + self::assertSame([], $this->subject->getAll()); + + $this->subject->add('foo', 'strtolower'); - $this->subject->registerHelper('foo', 'strtolower'); - self::assertEquals(['foo' => $this->mapExpectedCallable(strtolower(...))], $this->subject->getHelpers()); + self::assertEquals(['foo' => $this->mapExpectedCallable(strtolower(...))], $this->subject->getAll()); } #[Framework\Attributes\Test] - public function getHelpersReturnsRegisteredHelpers(): void + public function hasReturnsTrueIfGivenHelperIsRegistered(): void { - self::assertSame([], $this->subject->getHelpers()); + self::assertFalse($this->subject->has('foo')); - $this->subject->registerHelper('foo', 'strtolower'); - self::assertEquals(['foo' => $this->mapExpectedCallable(strtolower(...))], $this->subject->getHelpers()); + $this->subject->add('foo', 'strtolower'); + + self::assertTrue($this->subject->has('foo')); } /** * @return \Generator */ - public static function registerHelperLogsCriticalErrorIfGivenHelperIsInvalidDataProvider(): \Generator + public static function addLogsCriticalErrorIfGivenHelperIsInvalidDataProvider(): \Generator { yield 'null value' => [null]; yield 'non-callable function as string' => ['foo_baz']; @@ -108,37 +161,53 @@ public static function registerHelperLogsCriticalErrorIfGivenHelperIsInvalidData /** * @return \Generator */ - public static function registerHelperRegistersHelperCorrectlyDataProvider(): \Generator + public static function addRegistersHelperCorrectlyDataProvider(): \Generator { - yield 'callable function as string' => [ + yield 'callable as string' => [ 'trim', trim(...), ]; yield 'invokable class as string' => [ + Tests\Unit\Fixtures\Classes\Renderer\Helper\DummyInvokableHelper::class, + new Tests\Unit\Fixtures\Classes\Renderer\Helper\DummyInvokableHelper(), + ]; + yield 'class implementing Helper interface as string' => [ Tests\Unit\Fixtures\Classes\Renderer\Helper\DummyHelper::class, - new Tests\Unit\Fixtures\Classes\Renderer\Helper\DummyHelper(), + (new Tests\Unit\Fixtures\Classes\Renderer\Helper\DummyHelper())->render(...), + ]; + yield 'callable as closure' => [ + static fn() => 'foo', + static fn() => 'foo', + ]; + yield 'callable as first class callable syntax' => [ + trim(...), + trim(...), ]; yield 'invokable class as object' => [ + new Tests\Unit\Fixtures\Classes\Renderer\Helper\DummyInvokableHelper(), + new Tests\Unit\Fixtures\Classes\Renderer\Helper\DummyInvokableHelper(), + ]; + yield 'class implementing Helper interface as object' => [ new Tests\Unit\Fixtures\Classes\Renderer\Helper\DummyHelper(), - new Tests\Unit\Fixtures\Classes\Renderer\Helper\DummyHelper(), + (new Tests\Unit\Fixtures\Classes\Renderer\Helper\DummyHelper())->render(...), ]; - yield 'callable static class method' => [ + yield 'static class method as string' => [ Tests\Unit\Fixtures\Classes\Renderer\Helper\DummyHelper::class . '::staticExecute', [Tests\Unit\Fixtures\Classes\Renderer\Helper\DummyHelper::class, 'staticExecute'], ]; - yield 'callable non-static class method' => [ + yield 'non-static class method as string' => [ Tests\Unit\Fixtures\Classes\Renderer\Helper\DummyHelper::class . '::execute', [new Tests\Unit\Fixtures\Classes\Renderer\Helper\DummyHelper(), 'execute'], ]; - yield 'callable static class method in array syntax' => [ + yield 'static class method as array' => [ [Tests\Unit\Fixtures\Classes\Renderer\Helper\DummyHelper::class, 'staticExecute'], [Tests\Unit\Fixtures\Classes\Renderer\Helper\DummyHelper::class, 'staticExecute'], ]; - yield 'callable non-static class method in array syntax' => [ + yield 'non-static class method as array' => [ [Tests\Unit\Fixtures\Classes\Renderer\Helper\DummyHelper::class, 'execute'], [new Tests\Unit\Fixtures\Classes\Renderer\Helper\DummyHelper(), 'execute'], ]; - yield 'callable non-static class method in initialized array syntax' => [ + yield 'class method as initialized array' => [ [new Tests\Unit\Fixtures\Classes\Renderer\Helper\DummyHelper(), 'execute'], [new Tests\Unit\Fixtures\Classes\Renderer\Helper\DummyHelper(), 'execute'], ]; diff --git a/rector.php b/rector.php index c308af5d..b7e879ee 100644 --- a/rector.php +++ b/rector.php @@ -46,7 +46,7 @@ $rectorConfig->skip([ FirstClassCallableRector::class => [ __DIR__ . '/Tests/Functional/Renderer/Helper/RenderHelperTest.php', - __DIR__ . '/Tests/Unit/Traits/HandlebarsHelperTraitTest.php', + __DIR__ . '/Tests/Unit/Renderer/Helper/HelperRegistryTest.php', ], // @todo Remove once code is rewritten