diff --git a/Classes/DataProcessing/SimpleProcessor.php b/Classes/DataProcessing/SimpleProcessor.php index 4355d2db..80386123 100644 --- a/Classes/DataProcessing/SimpleProcessor.php +++ b/Classes/DataProcessing/SimpleProcessor.php @@ -25,6 +25,7 @@ use Fr\Typo3Handlebars\Exception\InvalidTemplateFileException; use Fr\Typo3Handlebars\Renderer\Renderer; +use Fr\Typo3Handlebars\Renderer\Template\View\HandlebarsView; use Fr\Typo3Handlebars\Traits\ErrorHandlingTrait; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; @@ -51,8 +52,12 @@ public function __construct( public function process(string $content, array $configuration): string { try { - $templatePath = $this->getTemplatePath($configuration); - return $this->renderer->render($templatePath, $this->contentObjectRenderer?->data ?? []); + $view = new HandlebarsView( + $this->getTemplatePath($configuration), + $this->contentObjectRenderer?->data ?? [], + ); + + return $this->renderer->render($view); } catch (InvalidTemplateFileException $exception) { $this->handleError($exception); return ''; diff --git a/Classes/Event/AfterRenderingEvent.php b/Classes/Event/AfterRenderingEvent.php index f27bc2b0..609c958b 100644 --- a/Classes/Event/AfterRenderingEvent.php +++ b/Classes/Event/AfterRenderingEvent.php @@ -34,14 +34,14 @@ final class AfterRenderingEvent { public function __construct( - private readonly string $templatePath, + private readonly Renderer\Template\View\HandlebarsView $view, private string $content, private readonly Renderer\Renderer $renderer, ) {} - public function getTemplatePath(): string + public function getView(): Renderer\Template\View\HandlebarsView { - return $this->templatePath; + return $this->view; } public function getContent(): string diff --git a/Classes/Event/BeforeRenderingEvent.php b/Classes/Event/BeforeRenderingEvent.php index 7a56a1ad..abf5cbe2 100644 --- a/Classes/Event/BeforeRenderingEvent.php +++ b/Classes/Event/BeforeRenderingEvent.php @@ -37,14 +37,14 @@ final class BeforeRenderingEvent * @param array $variables */ public function __construct( - private readonly string $templatePath, + private readonly Renderer\Template\View\HandlebarsView $view, private array $variables, private readonly Renderer\Renderer $renderer, ) {} - public function getTemplatePath(): string + public function getView(): Renderer\Template\View\HandlebarsView { - return $this->templatePath; + return $this->view; } /** diff --git a/Classes/Exception/PartialPathIsNotResolvable.php b/Classes/Exception/PartialPathIsNotResolvable.php index 4235dee7..befc1b24 100644 --- a/Classes/Exception/PartialPathIsNotResolvable.php +++ b/Classes/Exception/PartialPathIsNotResolvable.php @@ -31,10 +31,16 @@ */ final class PartialPathIsNotResolvable extends Exception { - public function __construct(string $path) + public function __construct(string $path, ?string $format = null) { + if ($format !== null) { + $formatMessage = sprintf(' with format "%s"', $format); + } else { + $formatMessage = ''; + } + parent::__construct( - \sprintf('The partial path "%s" cannot be resolved.', $path), + \sprintf('The partial path "%s"%s cannot be resolved.', $path, $formatMessage), 1736254715, ); } diff --git a/Classes/Exception/TemplateFileIsInvalid.php b/Classes/Exception/TemplateFileIsInvalid.php new file mode 100644 index 00000000..f49fac13 --- /dev/null +++ b/Classes/Exception/TemplateFileIsInvalid.php @@ -0,0 +1,41 @@ + + * + * 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; + +/** + * TemplateFileIsInvalid + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class TemplateFileIsInvalid extends Exception +{ + public function __construct(string $file) + { + parent::__construct( + \sprintf('The template file "%s" is invalid or does not exist.', $file), + 1736333208, + ); + } +} diff --git a/Classes/Exception/TemplateFormatIsNotSupported.php b/Classes/Exception/TemplateFormatIsNotSupported.php new file mode 100644 index 00000000..9a42ce12 --- /dev/null +++ b/Classes/Exception/TemplateFormatIsNotSupported.php @@ -0,0 +1,41 @@ + + * + * 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; + +/** + * TemplateFormatIsNotSupported + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class TemplateFormatIsNotSupported extends Exception +{ + public function __construct(string $format) + { + parent::__construct( + sprintf('The format "%s" is not supported by this template resolver.', $format), + 1736349135, + ); + } +} diff --git a/Classes/Exception/TemplatePathIsNotResolvable.php b/Classes/Exception/TemplatePathIsNotResolvable.php index bde3a493..93ce88ad 100644 --- a/Classes/Exception/TemplatePathIsNotResolvable.php +++ b/Classes/Exception/TemplatePathIsNotResolvable.php @@ -31,10 +31,16 @@ */ final class TemplatePathIsNotResolvable extends Exception { - public function __construct(string $path) + public function __construct(string $path, ?string $format = null) { + if ($format !== null) { + $formatMessage = \sprintf(' with format "%s"', $format); + } else { + $formatMessage = ''; + } + parent::__construct( - \sprintf('The template path "%s" cannot be resolved.', $path), + \sprintf('The template path "%s"%s cannot be resolved.', $path, $formatMessage), 1736254772, ); } diff --git a/Classes/Exception/ViewIsNotProperlyInitialized.php b/Classes/Exception/ViewIsNotProperlyInitialized.php new file mode 100644 index 00000000..2de3d422 --- /dev/null +++ b/Classes/Exception/ViewIsNotProperlyInitialized.php @@ -0,0 +1,41 @@ + + * + * 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; + +/** + * ViewIsNotProperlyInitialized + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class ViewIsNotProperlyInitialized extends Exception +{ + public function __construct() + { + parent::__construct( + 'The Handlebars view is not properly initialized. Provide either template path or template source.', + 1736332788, + ); + } +} diff --git a/Classes/Renderer/HandlebarsRenderer.php b/Classes/Renderer/HandlebarsRenderer.php index e9833d08..3902e275 100644 --- a/Classes/Renderer/HandlebarsRenderer.php +++ b/Classes/Renderer/HandlebarsRenderer.php @@ -61,11 +61,11 @@ public function __construct( $this->debugMode = $this->isDebugModeEnabled(); } - public function render(string $templatePath, array $data = []): string + public function render(Template\View\HandlebarsView $view): string { try { - return $this->processRendering($templatePath, $data); - } catch (Exception\InvalidTemplateFileException | Exception\TemplateCompilationException | Exception\TemplatePathIsNotResolvable $exception) { + return $this->processRendering($view); + } catch (Exception\TemplateCompilationException | Exception\TemplateFileIsInvalid | Exception\TemplateFormatIsNotSupported | Exception\TemplatePathIsNotResolvable | Exception\ViewIsNotProperlyInitialized $exception) { $this->logger->critical($exception->getMessage(), ['exception' => $exception]); return ''; @@ -73,45 +73,36 @@ public function render(string $templatePath, array $data = []): string } /** - * @param array $variables - * @throws Exception\InvalidTemplateFileException - * @throws Exception\TemplateCompilationException + * @throws Exception\TemplateFileIsInvalid + * @throws Exception\TemplateFormatIsNotSupported * @throws Exception\TemplatePathIsNotResolvable + * @throws Exception\ViewIsNotProperlyInitialized */ - protected function processRendering(string $templatePath, array $variables): string + protected function processRendering(Template\View\HandlebarsView $view): string { - $fullTemplatePath = $this->templateResolver->resolveTemplatePath($templatePath); - $template = file_get_contents($fullTemplatePath); - - // Throw exception if template file is invalid - if ($template === false) { - throw new Exception\InvalidTemplateFileException($fullTemplatePath, 1606217313); - } + $compileResult = $this->compile($view); // Early return if template is empty - if (trim($template) === '') { + if ($compileResult === null) { return ''; } // Merge variables with default variables - $mergedVariables = array_merge($this->variableBag->get(), $variables); - - // Compile template - $compileResult = $this->compile($template); - $renderer = $this->prepareCompileResult($compileResult); + $mergedVariables = array_merge($this->variableBag->get(), $view->getVariables()); // Dispatch before rendering event - $beforeRenderingEvent = new Event\BeforeRenderingEvent($fullTemplatePath, $mergedVariables, $this); + $beforeRenderingEvent = new Event\BeforeRenderingEvent($view, $mergedVariables, $this); $this->eventDispatcher->dispatch($beforeRenderingEvent); // Render content + $renderer = $this->prepareCompileResult($compileResult); $content = $renderer($beforeRenderingEvent->getVariables(), [ 'debug' => Runtime::DEBUG_TAGS_HTML, 'helpers' => $this->helperRegistry->getAll(), ]); // Dispatch after rendering event - $afterRenderingEvent = new Event\AfterRenderingEvent($fullTemplatePath, $content, $this); + $afterRenderingEvent = new Event\AfterRenderingEvent($view, $content, $this); $this->eventDispatcher->dispatch($afterRenderingEvent); return $afterRenderingEvent->getContent(); @@ -120,16 +111,25 @@ protected function processRendering(string $templatePath, array $variables): str /** * Compile given template by LightnCandy compiler. * - * @param string $template Raw template to be compiled - * @return string The compiled template - * @throws Exception\TemplateCompilationException() if template compilation fails and errors are not yet handled by compiler + * @throws Exception\TemplateFileIsInvalid + * @throws Exception\TemplateFormatIsNotSupported + * @throws Exception\TemplatePathIsNotResolvable + * @throws Exception\ViewIsNotProperlyInitialized */ - protected function compile(string $template): string + protected function compile(Template\View\HandlebarsView $view): ?string { + $template = $view->getTemplate($this->templateResolver); + + // Early return if template is empty + if (\trim($template) === '') { + return null; + } + // Disable cache if debugging is enabled or caching is disabled - $cache = $this->cache; if ($this->debugMode || $this->isCachingDisabled()) { $cache = new Cache\NullCache(); + } else { + $cache = $this->cache; } // Get compile result from cache @@ -239,6 +239,7 @@ protected function getHelperStubs(): array * @param string $name Name of the partial to be resolved * @return string|null Partial file contents if partial could be resolved, `null` otherwise * @throws Exception\PartialPathIsNotResolvable + * @throws Exception\TemplateFormatIsNotSupported */ public function resolvePartial(array $context, string $name): ?string { diff --git a/Classes/Renderer/Helper/ExtendHelper.php b/Classes/Renderer/Helper/ExtendHelper.php index 41535f93..426d0e2e 100644 --- a/Classes/Renderer/Helper/ExtendHelper.php +++ b/Classes/Renderer/Helper/ExtendHelper.php @@ -60,9 +60,11 @@ public function render(Context\HelperContext $context): string $renderingContext['_layoutStack'][] = $handlebarsLayout; // Merge data with supplied data - $renderData = array_replace_recursive($renderingContext, $customContext, $context->hash); + $variables = array_replace_recursive($renderingContext, $customContext, $context->hash); // Render layout with merged data - return $this->renderer->render($name, $renderData); + return $this->renderer->render( + new Renderer\Template\View\HandlebarsView($name, $variables), + ); } } diff --git a/Classes/Renderer/Helper/RenderHelper.php b/Classes/Renderer/Helper/RenderHelper.php index 3d130865..7a1936b1 100644 --- a/Classes/Renderer/Helper/RenderHelper.php +++ b/Classes/Renderer/Helper/RenderHelper.php @@ -86,7 +86,9 @@ public function render(Context\HelperContext $context): SafeString if ($renderUncached) { $content = $this->registerUncachedTemplateBlock($name, $subContext); } else { - $content = $this->renderer->render($name, $subContext); + $content = $this->renderer->render( + new Renderer\Template\View\HandlebarsView($name, $subContext), + ); } return new SafeString($content); diff --git a/Classes/Renderer/Renderer.php b/Classes/Renderer/Renderer.php index acf41955..6a5651d5 100644 --- a/Classes/Renderer/Renderer.php +++ b/Classes/Renderer/Renderer.php @@ -31,8 +31,5 @@ */ interface Renderer { - /** - * @param array $data - */ - public function render(string $templatePath, array $data = []): string; + public function render(Template\View\HandlebarsView $view): string; } diff --git a/Classes/Renderer/Template/FlatTemplateResolver.php b/Classes/Renderer/Template/FlatTemplateResolver.php index 29a62309..4d2e6a34 100644 --- a/Classes/Renderer/Template/FlatTemplateResolver.php +++ b/Classes/Renderer/Template/FlatTemplateResolver.php @@ -66,28 +66,39 @@ public function __construct( $this->flattenedTemplates = $this->buildPathMap($this->templateRootPaths); } - public function resolvePartialPath(string $partialPath): string + public function resolvePartialPath(string $partialPath, ?string $format = null): string { - return $this->resolvePath($partialPath, $this->flattenedPartials) - ?? $this->fallbackResolver->resolvePartialPath($partialPath); + return $this->resolvePath($partialPath, $this->flattenedPartials, $format) + ?? $this->fallbackResolver->resolvePartialPath($partialPath, $format); } - public function resolveTemplatePath(string $templatePath): string + public function resolveTemplatePath(string $templatePath, ?string $format = null): string { - return $this->resolvePath($templatePath, $this->flattenedTemplates) - ?? $this->fallbackResolver->resolveTemplatePath($templatePath); + return $this->resolvePath($templatePath, $this->flattenedTemplates, $format) + ?? $this->fallbackResolver->resolveTemplatePath($templatePath, $format); } /** * @param array $flattenedFiles + * @throws Exception\TemplateFormatIsNotSupported */ - private function resolvePath(string $path, array $flattenedFiles): ?string + private function resolvePath(string $path, array $flattenedFiles, ?string $format = null): ?string { + // Throw exception if given format is not supported + if ($format !== null && !\in_array($format, $this->supportedFileExtensions, true)) { + throw new Exception\TemplateFormatIsNotSupported($format); + } + // Use default path resolving if path is not prefixed by "@" if (!str_starts_with($path, '@')) { return null; } + // Append format if requested + if ($format !== null) { + $path .= '.' . $format; + } + // Strip "@" prefix from given template path $templateName = ltrim($path, '@'); @@ -138,6 +149,10 @@ private function buildPathMap(array $rootPaths): array if ($this->isFirstOccurrenceInRootPaths($flattenedPaths, $file)) { $flattenedPaths[$this->resolveFlatFilename($file)] = $file; } + + if ($this->isFirstOccurrenceInRootPaths($flattenedPaths, $file, true)) { + $flattenedPaths[$this->resolveFlatFilename($file, true)] = $file; + } } } @@ -147,9 +162,12 @@ private function buildPathMap(array $rootPaths): array /** * @param array $flattenedPaths */ - private function isFirstOccurrenceInRootPaths(array $flattenedPaths, Finder\SplFileInfo $file): bool - { - $filename = $this->resolveFlatFilename($file); + private function isFirstOccurrenceInRootPaths( + array $flattenedPaths, + Finder\SplFileInfo $file, + bool $withExtension = false, + ): bool { + $filename = $this->resolveFlatFilename($file, $withExtension); // Early return if template is not registered yet if (!isset($flattenedPaths[$filename])) { @@ -162,9 +180,16 @@ private function isFirstOccurrenceInRootPaths(array $flattenedPaths, Finder\SplF return $flattenedPaths[$filename]->getRelativePathname() === $file->getRelativePathname(); } - private function resolveFlatFilename(Finder\SplFileInfo $file): string + private function resolveFlatFilename(Finder\SplFileInfo $file, bool $withExtension = false): string { - return pathinfo($file->getPathname(), PATHINFO_FILENAME); + $pathname = $file->getPathname(); + $filename = pathinfo($pathname, PATHINFO_FILENAME); + + if ($withExtension) { + $filename = $filename . '.' . pathinfo($pathname, PATHINFO_EXTENSION); + } + + return $filename; } /** diff --git a/Classes/Renderer/Template/HandlebarsTemplateResolver.php b/Classes/Renderer/Template/HandlebarsTemplateResolver.php index 8566880b..fcb7edb6 100644 --- a/Classes/Renderer/Template/HandlebarsTemplateResolver.php +++ b/Classes/Renderer/Template/HandlebarsTemplateResolver.php @@ -46,35 +46,47 @@ public function __construct( $this->supportedFileExtensions = $this->resolveSupportedFileExtensions($supportedFileExtensions); } - public function resolvePartialPath(string $partialPath): string + public function resolvePartialPath(string $partialPath, ?string $format = null): string { - return $this->resolvePath($partialPath, $this->partialRootPaths) - ?? throw new Exception\PartialPathIsNotResolvable($partialPath); + return $this->resolvePath($partialPath, $this->partialRootPaths, $format) + ?? throw new Exception\PartialPathIsNotResolvable($partialPath, $format); } - public function resolveTemplatePath(string $templatePath): string + public function resolveTemplatePath(string $templatePath, ?string $format = null): string { - return $this->resolvePath($templatePath, $this->templateRootPaths) - ?? throw new Exception\TemplatePathIsNotResolvable($templatePath); + return $this->resolvePath($templatePath, $this->templateRootPaths, $format) + ?? throw new Exception\TemplatePathIsNotResolvable($templatePath, $format); } /** * @param list $rootPaths + * @throws Exception\TemplateFormatIsNotSupported */ - private function resolvePath(string $path, array $rootPaths): ?string + private function resolvePath(string $path, array $rootPaths, ?string $format = null): ?string { + $fileExtensions = $this->supportedFileExtensions; $filename = $path; + if ($format !== null) { + // Throw exception if given format is not supported + if (!in_array($format, $fileExtensions, true)) { + throw new Exception\TemplateFormatIsNotSupported($format); + } + + $fileExtensions = [$format]; + } + foreach (array_reverse($rootPaths) as $rootPath) { - foreach ($this->supportedFileExtensions as $extension) { + foreach ($fileExtensions as $extension) { $possibleFilename = $this->resolveFilename($path, $rootPath, $extension); + if (is_file($possibleFilename)) { return $possibleFilename; } } } - if (is_file($possibleFilename = $this->resolveFilename($filename))) { + if ($format === null && is_file($possibleFilename = $this->resolveFilename($filename))) { return $possibleFilename; } diff --git a/Classes/Renderer/Template/TemplateResolver.php b/Classes/Renderer/Template/TemplateResolver.php index b27aac4b..1242da47 100644 --- a/Classes/Renderer/Template/TemplateResolver.php +++ b/Classes/Renderer/Template/TemplateResolver.php @@ -42,13 +42,15 @@ public function supports(string $fileExtension): bool; * Resolve given partial path to the full partial path including the base partial path. * * @throws Exception\PartialPathIsNotResolvable + * @throws Exception\TemplateFormatIsNotSupported */ - public function resolvePartialPath(string $partialPath): string; + public function resolvePartialPath(string $partialPath, ?string $format = null): string; /** * Resolve given template path to the full template path including the base template path. * + * @throws Exception\TemplateFormatIsNotSupported * @throws Exception\TemplatePathIsNotResolvable */ - public function resolveTemplatePath(string $templatePath): string; + public function resolveTemplatePath(string $templatePath, ?string $format = null): string; } diff --git a/Classes/Renderer/Template/View/HandlebarsView.php b/Classes/Renderer/Template/View/HandlebarsView.php new file mode 100644 index 00000000..6c9ba006 --- /dev/null +++ b/Classes/Renderer/Template/View/HandlebarsView.php @@ -0,0 +1,144 @@ + + * + * 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\View; + +use Fr\Typo3Handlebars\Exception; +use Fr\Typo3Handlebars\Renderer; + +/** + * HandlebarsView + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class HandlebarsView +{ + private ?string $templateSource = null; + private ?string $format = null; + + /** + * @param array $variables + */ + public function __construct( + private ?string $templatePath = null, + private array $variables = [], + ) {} + + /** + * @throws Exception\TemplateFileIsInvalid + * @throws Exception\TemplateFormatIsNotSupported + * @throws Exception\TemplatePathIsNotResolvable + * @throws Exception\ViewIsNotProperlyInitialized + */ + public function getTemplate(?Renderer\Template\TemplateResolver $templateResolver = null): string + { + if ($this->templateSource !== null) { + return $this->templateSource; + } + + if ($this->templatePath === null) { + throw new Exception\ViewIsNotProperlyInitialized(); + } + + if ($templateResolver !== null) { + $fullTemplatePath = $templateResolver->resolveTemplatePath($this->templatePath, $this->format); + } else { + $format = $this->format !== null ? '.' . $this->format : ''; + $fullTemplatePath = $this->templatePath . $format; + } + + return $this->fetchFromFile($fullTemplatePath); + } + + /** + * @throws Exception\TemplateFileIsInvalid + */ + private function fetchFromFile(string $file): string + { + if (!\is_file($file)) { + throw new Exception\TemplateFileIsInvalid($file); + } + + $template = @file_get_contents($file); + + if ($template === false) { + throw new Exception\TemplateFileIsInvalid($file); + } + + return $template; + } + + public function setTemplatePath(string $templatePath): self + { + $this->templatePath = $templatePath; + + return $this; + } + + public function setTemplateSource(string $templateSource): self + { + $this->templateSource = $templateSource; + + return $this; + } + + /** + * @return array + */ + public function getVariables(): array + { + return $this->variables; + } + + public function assign(string $key, mixed $value): self + { + $this->variables[$key] = $value; + + return $this; + } + + /** + * @param array $values + */ + public function assignMultiple(array $values): self + { + foreach ($values as $key => $value) { + $this->assign($key, $value); + } + + return $this; + } + + public function getFormat(): ?string + { + return $this->format; + } + + public function setFormat(string $format): self + { + $this->format = $format; + + return $this; + } +} diff --git a/Tests/Functional/Fixtures/test_extension/Resources/Partials/foo.html b/Tests/Functional/Fixtures/test_extension/Resources/Partials/foo.html new file mode 100644 index 00000000..e69de29b diff --git a/Tests/Functional/Fixtures/test_extension/Resources/Templates/foo.html b/Tests/Functional/Fixtures/test_extension/Resources/Templates/foo.html new file mode 100644 index 00000000..e69de29b diff --git a/Tests/Functional/Renderer/Helper/BlockHelperTest.php b/Tests/Functional/Renderer/Helper/BlockHelperTest.php index bcee7e3d..fc263cad 100644 --- a/Tests/Functional/Renderer/Helper/BlockHelperTest.php +++ b/Tests/Functional/Renderer/Helper/BlockHelperTest.php @@ -76,7 +76,10 @@ protected function setUp(): void #[Framework\Attributes\Test] public function helperCanBeCalledFromMainLayout(): void { - $actual = trim($this->renderer->render('@main-layout')); + $actual = $this->renderer->render( + new Src\Renderer\Template\View\HandlebarsView('@main-layout'), + ); + $expected = implode(PHP_EOL, [ 'this is the main block:', '', @@ -97,15 +100,21 @@ public function helperCanBeCalledFromMainLayout(): void 'this is the end. bye bye', ]); - self::assertMatchesRegularExpression('/^' . $expected . '$/', $actual); + self::assertMatchesRegularExpression('/^' . $expected . '$/', trim($actual)); } #[Framework\Attributes\Test] public function helperCanBeCalledFromExtendedLayout(): void { - $actual = trim($this->renderer->render('@main-layout-extended-with-fifth-content', [ - 'templateName' => '@main-layout', - ])); + $actual = $this->renderer->render( + new Src\Renderer\Template\View\HandlebarsView( + '@main-layout-extended-with-fifth-content', + [ + 'templateName' => '@main-layout', + ], + ), + ); + $expected = implode(PHP_EOL, [ 'this is the main block:', '', @@ -131,6 +140,6 @@ public function helperCanBeCalledFromExtendedLayout(): void 'this is the end. bye bye', ]); - self::assertMatchesRegularExpression('/^' . $expected . '$/', $actual); + self::assertMatchesRegularExpression('/^' . $expected . '$/', trim($actual)); } } diff --git a/Tests/Functional/Renderer/Helper/ContentHelperTest.php b/Tests/Functional/Renderer/Helper/ContentHelperTest.php index 46ab2d4d..a33d5f47 100644 --- a/Tests/Functional/Renderer/Helper/ContentHelperTest.php +++ b/Tests/Functional/Renderer/Helper/ContentHelperTest.php @@ -78,9 +78,15 @@ protected function setUp(): void #[Framework\Attributes\Test] public function helperCanBeCalledFromExtendedLayout(): void { - $actual = trim($this->renderer->render('@main-layout-extended', [ - 'templateName' => '@main-layout', - ])); + $actual = $this->renderer->render( + new Src\Renderer\Template\View\HandlebarsView( + '@main-layout-extended', + [ + 'templateName' => '@main-layout', + ], + ), + ); + $expected = implode(PHP_EOL, [ 'this is the main block:', '', @@ -103,13 +109,15 @@ public function helperCanBeCalledFromExtendedLayout(): void 'this is the end. bye bye', ]); - self::assertMatchesRegularExpression('/^' . $expected . '$/', $actual); + self::assertMatchesRegularExpression('/^' . $expected . '$/', trim($actual)); } #[Framework\Attributes\Test] public function helperCannotBeCalledOutsideOfExtendedLayout(): void { - $this->renderer->render('@main-layout-content-only'); + $this->renderer->render( + new Src\Renderer\Template\View\HandlebarsView('@main-layout-content-only'), + ); self::assertTrue( $this->logger->hasError([ @@ -124,9 +132,14 @@ public function helperCannotBeCalledOutsideOfExtendedLayout(): void #[Framework\Attributes\Test] public function helperUsesReplaceModeIfInvalidModeIsGiven(): void { - $this->renderer->render('@main-layout-extended-with-invalid-mode', [ - 'templateName' => '@main-layout', - ]); + $this->renderer->render( + new Src\Renderer\Template\View\HandlebarsView( + '@main-layout-extended-with-invalid-mode', + [ + 'templateName' => '@main-layout', + ], + ), + ); self::assertTrue( $this->logger->hasWarning([ @@ -142,12 +155,17 @@ public function helperUsesReplaceModeIfInvalidModeIsGiven(): void #[Framework\Attributes\DataProvider('helperCanBeCalledToConditionallyRenderBlocksDataProvider')] public function helperCanBeCalledToConditionallyRenderBlocks(bool $renderSecondBlock, string $expected): void { - $actual = trim($this->renderer->render('@main-layout-extended-with-conditional-contents', [ - 'templateName' => '@main-layout-conditional-block', - 'renderSecondBlock' => $renderSecondBlock, - ])); + $actual = $this->renderer->render( + new Src\Renderer\Template\View\HandlebarsView( + '@main-layout-extended-with-conditional-contents', + [ + 'templateName' => '@main-layout-conditional-block', + 'renderSecondBlock' => $renderSecondBlock, + ], + ), + ); - self::assertMatchesRegularExpression('/^' . $expected . '$/', $actual); + self::assertMatchesRegularExpression('/^' . $expected . '$/', trim($actual)); } /** diff --git a/Tests/Functional/Renderer/Helper/ExtendHelperTest.php b/Tests/Functional/Renderer/Helper/ExtendHelperTest.php index 3c5f8ebc..3628a6f5 100644 --- a/Tests/Functional/Renderer/Helper/ExtendHelperTest.php +++ b/Tests/Functional/Renderer/Helper/ExtendHelperTest.php @@ -76,25 +76,36 @@ protected function setUp(): void #[Framework\Attributes\Test] public function helperCanBeCalledWithoutCustomContext(): void { - $actual = trim($this->renderer->render('@simple-layout-extended')); - $expected = []; + $actual = trim( + $this->renderer->render( + new Src\Renderer\Template\View\HandlebarsView('@simple-layout-extended'), + ), + ); self::assertJson($actual); $json = json_decode($actual, true); unset($json['_layoutStack']); - self::assertSame($expected, $json); + self::assertSame([], $json); } #[Framework\Attributes\Test] public function helperCanBeCalledWithCustomContext(): void { - $actual = trim($this->renderer->render('@simple-layout-extended-with-context', [ - 'customContext' => [ - 'foo' => 'baz', - ], - ])); + $actual = trim( + $this->renderer->render( + new Src\Renderer\Template\View\HandlebarsView( + '@simple-layout-extended-with-context', + [ + 'customContext' => [ + 'foo' => 'baz', + ], + ], + ), + ), + ); + $expected = [ 'customContext' => [ 'foo' => 'baz', @@ -113,12 +124,19 @@ public function helperCanBeCalledWithCustomContext(): void #[Framework\Attributes\Test] public function helperReplacesVariablesCorrectlyInAllContexts(): void { - $actual = trim($this->renderer->render('@simple-layout-extended-with-context', [ - 'foo' => 123, - 'customContext' => [ - 'foo' => 456, - ], - ])); + $actual = trim( + $this->renderer->render( + new Src\Renderer\Template\View\HandlebarsView( + '@simple-layout-extended-with-context', + [ + 'foo' => 123, + 'customContext' => [ + 'foo' => 456, + ], + ], + ), + ), + ); $expected = [ 'foo' => 456, diff --git a/Tests/Functional/Renderer/Helper/RenderHelperTest.php b/Tests/Functional/Renderer/Helper/RenderHelperTest.php index 2f378643..15a2bc00 100644 --- a/Tests/Functional/Renderer/Helper/RenderHelperTest.php +++ b/Tests/Functional/Renderer/Helper/RenderHelperTest.php @@ -87,11 +87,16 @@ protected function setUp(): void #[Framework\Attributes\Test] public function helperCanBeCalledWithDefaultContext(): void { - $actual = $this->renderer->render('@render-default-context', [ - '@foo' => [ - 'renderedContent' => 'Hello world!', - ], - ]); + $actual = $this->renderer->render( + new Src\Renderer\Template\View\HandlebarsView( + '@render-default-context', + [ + '@foo' => [ + 'renderedContent' => 'Hello world!', + ], + ], + ), + ); self::assertSame('Hello world!', trim($actual)); } @@ -99,11 +104,16 @@ public function helperCanBeCalledWithDefaultContext(): void #[Framework\Attributes\Test] public function helperCanBeCalledWithCustomContext(): void { - $actual = $this->renderer->render('@render-custom-context', [ - 'renderData' => [ - 'renderedContent' => 'Hello world!', - ], - ]); + $actual = $this->renderer->render( + new Src\Renderer\Template\View\HandlebarsView( + '@render-custom-context', + [ + 'renderData' => [ + 'renderedContent' => 'Hello world!', + ], + ], + ), + ); self::assertSame('Hello world!', trim($actual)); } @@ -111,14 +121,19 @@ public function helperCanBeCalledWithCustomContext(): void #[Framework\Attributes\Test] public function helperCanBeCalledWithMergedContext(): void { - $actual = $this->renderer->render('@render-merged-context', [ - '@foo' => [ - 'renderedContent' => 'Hello world!', - ], - 'renderData' => [ - 'renderedContent' => 'Lorem ipsum', - ], - ]); + $actual = $this->renderer->render( + new Src\Renderer\Template\View\HandlebarsView( + '@render-merged-context', + [ + '@foo' => [ + 'renderedContent' => 'Hello world!', + ], + 'renderData' => [ + 'renderedContent' => 'Lorem ipsum', + ], + ], + ), + ); self::assertSame('Lorem ipsum', trim($actual)); } @@ -135,12 +150,17 @@ public function helperCanBeCalledToRenderANonCacheableTemplate(): void ); $GLOBALS['TSFE']->cObj = $this->contentObjectRenderer; - $actual = $GLOBALS['TSFE']->content = $this->renderer->render('@render-uncached', [ - 'renderData' => [ - '_processor' => TestExtension\DummyNonCacheableProcessor::class, - 'foo' => 'baz', - ], - ]); + $actual = $GLOBALS['TSFE']->content = $this->renderer->render( + new Src\Renderer\Template\View\HandlebarsView( + '@render-uncached', + [ + 'renderData' => [ + '_processor' => TestExtension\DummyNonCacheableProcessor::class, + 'foo' => 'baz', + ], + ], + ), + ); self::assertMatchesRegularExpression('#^$#', trim($actual)); diff --git a/Tests/Functional/Renderer/Template/FlatTemplateResolverTest.php b/Tests/Functional/Renderer/Template/FlatTemplateResolverTest.php index 5b0a4cf5..9b46f3cf 100644 --- a/Tests/Functional/Renderer/Template/FlatTemplateResolverTest.php +++ b/Tests/Functional/Renderer/Template/FlatTemplateResolverTest.php @@ -115,6 +115,24 @@ public function resolvePartialPathFallsBackToDefaultResolverIfPartialPathDoesNot self::assertSame($expected, $this->getTemplateResolver()->resolvePartialPath('foo')); } + #[Framework\Attributes\Test] + public function resolvePartialPathThrowsExceptionIfGivenFormatIsNotSupported(): void + { + $this->expectExceptionObject( + new Src\Exception\TemplateFormatIsNotSupported('foo'), + ); + + $this->getTemplateResolver()->resolvePartialPath('@foo', 'foo'); + } + + #[Framework\Attributes\Test] + public function resolvePartialPathRespectsFormat(): void + { + $expected = $this->instancePath . '/typo3conf/ext/test_extension/Resources/Partials/foo.html'; + + self::assertSame($expected, $this->getTemplateResolver()->resolvePartialPath('@foo', 'html')); + } + #[Framework\Attributes\Test] public function resolvePartialPathFallsBackToDefaultResolverIfPartialPathCannotBeResolved(): void { @@ -150,6 +168,24 @@ public function resolveTemplatePathFallsBackToDefaultResolverIfTemplatePathDoesN self::assertSame($expected, $this->getTemplateResolver()->resolveTemplatePath('main-layout')); } + #[Framework\Attributes\Test] + public function resolveTemplatePathThrowsExceptionIfGivenFormatIsNotSupported(): void + { + $this->expectExceptionObject( + new Src\Exception\TemplateFormatIsNotSupported('foo'), + ); + + $this->getTemplateResolver()->resolveTemplatePath('@foo', 'foo'); + } + + #[Framework\Attributes\Test] + public function resolveTemplatePathRespectsFormat(): void + { + $expected = $this->instancePath . '/typo3conf/ext/test_extension/Resources/Templates/foo.html'; + + self::assertSame($expected, $this->getTemplateResolver()->resolveTemplatePath('@foo', 'html')); + } + #[Framework\Attributes\Test] public function resolveTemplatePathFallsBackToDefaultResolverIfTemplatePathCannotBeResolved(): void { diff --git a/Tests/Unit/Event/AfterRenderingEventTest.php b/Tests/Unit/Event/AfterRenderingEventTest.php index e86bfde3..bddb4ec3 100644 --- a/Tests/Unit/Event/AfterRenderingEventTest.php +++ b/Tests/Unit/Event/AfterRenderingEventTest.php @@ -43,16 +43,19 @@ protected function setUp(): void parent::setUp(); $this->subject = new Src\Event\AfterRenderingEvent( - 'foo', + new Src\Renderer\Template\View\HandlebarsView('foo'), 'baz', $this->createMock(Src\Renderer\HandlebarsRenderer::class), ); } #[Framework\Attributes\Test] - public function getTemplatePathReturnsTemplatePath(): void + public function getViewReturnsHandlebarsView(): void { - self::assertSame('foo', $this->subject->getTemplatePath()); + self::assertEquals( + new Src\Renderer\Template\View\HandlebarsView('foo'), + $this->subject->getView(), + ); } #[Framework\Attributes\Test] diff --git a/Tests/Unit/Event/BeforeRenderingEventTest.php b/Tests/Unit/Event/BeforeRenderingEventTest.php index 85d74f10..da240953 100644 --- a/Tests/Unit/Event/BeforeRenderingEventTest.php +++ b/Tests/Unit/Event/BeforeRenderingEventTest.php @@ -43,16 +43,19 @@ protected function setUp(): void parent::setUp(); $this->subject = new Src\Event\BeforeRenderingEvent( - 'foo', + new Src\Renderer\Template\View\HandlebarsView('foo'), ['foo' => 'baz'], $this->createMock(Src\Renderer\HandlebarsRenderer::class), ); } #[Framework\Attributes\Test] - public function getTemplatePathReturnsTemplatePath(): void + public function getViewReturnsHandlebarsView(): void { - self::assertSame('foo', $this->subject->getTemplatePath()); + self::assertEquals( + new Src\Renderer\Template\View\HandlebarsView('foo'), + $this->subject->getView(), + ); } #[Framework\Attributes\Test] diff --git a/Tests/Unit/Exception/PartialPathIsNotResolvableTest.php b/Tests/Unit/Exception/PartialPathIsNotResolvableTest.php new file mode 100644 index 00000000..ec9eee70 --- /dev/null +++ b/Tests/Unit/Exception/PartialPathIsNotResolvableTest.php @@ -0,0 +1,56 @@ + + * + * 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; + +/** + * PartialPathIsNotResolvableTest + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +#[Framework\Attributes\CoversClass(Src\Exception\PartialPathIsNotResolvable::class)] +final class PartialPathIsNotResolvableTest extends TestingFramework\Core\Unit\UnitTestCase +{ + #[Framework\Attributes\Test] + public function constructorReturnsExceptionForGivenPath(): void + { + $actual = new Src\Exception\PartialPathIsNotResolvable('foo'); + + self::assertSame('The partial path "foo" cannot be resolved.', $actual->getMessage()); + self::assertSame(1736254715, $actual->getCode()); + } + + #[Framework\Attributes\Test] + public function constructorReturnsExceptionForGivenPathAndFormat(): void + { + $actual = new Src\Exception\PartialPathIsNotResolvable('foo', 'json'); + + self::assertSame('The partial path "foo" with format "json" cannot be resolved.', $actual->getMessage()); + self::assertSame(1736254715, $actual->getCode()); + } +} diff --git a/Tests/Unit/Exception/TemplatePathIsNotResolvableTest.php b/Tests/Unit/Exception/TemplatePathIsNotResolvableTest.php new file mode 100644 index 00000000..b3a8afda --- /dev/null +++ b/Tests/Unit/Exception/TemplatePathIsNotResolvableTest.php @@ -0,0 +1,56 @@ + + * + * 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; + +/** + * TemplatePathIsNotResolvableTest + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +#[Framework\Attributes\CoversClass(Src\Exception\TemplatePathIsNotResolvable::class)] +final class TemplatePathIsNotResolvableTest extends TestingFramework\Core\Unit\UnitTestCase +{ + #[Framework\Attributes\Test] + public function constructorReturnsExceptionForGivenPath(): void + { + $actual = new Src\Exception\TemplatePathIsNotResolvable('foo'); + + self::assertSame('The template path "foo" cannot be resolved.', $actual->getMessage()); + self::assertSame(1736254772, $actual->getCode()); + } + + #[Framework\Attributes\Test] + public function constructorReturnsExceptionForGivenPathAndFormat(): void + { + $actual = new Src\Exception\TemplatePathIsNotResolvable('foo', 'json'); + + self::assertSame('The template path "foo" with format "json" cannot be resolved.', $actual->getMessage()); + self::assertSame(1736254772, $actual->getCode()); + } +} diff --git a/Tests/Unit/Fixtures/Classes/Renderer/Template/DummyTemplateResolver.php b/Tests/Unit/Fixtures/Classes/Renderer/Template/DummyTemplateResolver.php index bd869650..218c6ba7 100644 --- a/Tests/Unit/Fixtures/Classes/Renderer/Template/DummyTemplateResolver.php +++ b/Tests/Unit/Fixtures/Classes/Renderer/Template/DummyTemplateResolver.php @@ -42,12 +42,12 @@ public function __construct() $this->root = vfs\vfsStream::setup(uniqid('ext_handlebars_test_')); } - public function resolvePartialPath(string $partialPath): string + public function resolvePartialPath(string $partialPath, ?string $format = null): string { return vfs\vfsStream::newFile($partialPath, 0000)->at($this->root)->url(); } - public function resolveTemplatePath(string $templatePath): string + public function resolveTemplatePath(string $templatePath, ?string $format = null): string { return vfs\vfsStream::newFile($templatePath, 0000)->at($this->root)->url(); } diff --git a/Tests/Unit/Fixtures/Partials/DummyPartial.html b/Tests/Unit/Fixtures/Partials/DummyPartial.html new file mode 100644 index 00000000..e69de29b diff --git a/Tests/Unit/Fixtures/Templates/DummyTemplate.html b/Tests/Unit/Fixtures/Templates/DummyTemplate.html new file mode 100644 index 00000000..e69de29b diff --git a/Tests/Unit/Renderer/HandlebarsRendererTest.php b/Tests/Unit/Renderer/HandlebarsRendererTest.php index 48f3bab7..81278bdf 100644 --- a/Tests/Unit/Renderer/HandlebarsRendererTest.php +++ b/Tests/Unit/Renderer/HandlebarsRendererTest.php @@ -60,9 +60,55 @@ protected function setUp(): void } #[Framework\Attributes\Test] - public function renderLogsCriticalErrorIfGivenTemplateIsNotAvailable(): void + public function renderLogsCriticalErrorIfTemplateCompilationFails(): void + { + $view = new Src\Renderer\Template\View\HandlebarsView('DummyTemplateErroneous'); + + self::assertSame( + '', + $this->renewSubject(Tests\Unit\Fixtures\Classes\Renderer\DummyRenderer::class)->render($view), + ); + self::assertTrue($this->logger->hasCriticalThatPasses(function ($logRecord) { + /** @var Src\Exception\TemplateCompilationException $exception */ + $exception = $logRecord['context']['exception']; + + self::assertInstanceOf(Src\Exception\TemplateCompilationException::class, $exception); + self::assertSame(1614620212, $exception->getCode()); + + return true; + })); + } + + #[Framework\Attributes\Test] + public function renderLogsCriticalErrorIfTemplateFilsIsInvalid(): void + { + $view = new Src\Renderer\Template\View\HandlebarsView('foo.baz'); + + $this->templateResolver = new Tests\Unit\Fixtures\Classes\Renderer\Template\DummyTemplateResolver(); + + $this->suppressNextError(E_WARNING); + + self::assertSame('', $this->renewSubject()->render($view)); + self::assertTrue($this->logger->hasCriticalThatPasses(function ($logRecord) { + $exception = $logRecord['context']['exception']; + + self::assertInstanceOf(Src\Exception\TemplateFileIsInvalid::class, $exception); + self::assertSame(1736333208, $exception->getCode()); + self::assertMatchesRegularExpression( + '/^The template file "[^"]+\/foo\.baz" is invalid or does not exist\.$/', + $exception->getMessage(), + ); + + return true; + })); + } + + #[Framework\Attributes\Test] + public function renderLogsCriticalErrorIfTemplatePathIsNotResolvable(): void { - self::assertSame('', $this->subject->render('foo.baz')); + $view = new Src\Renderer\Template\View\HandlebarsView('foo.baz'); + + self::assertSame('', $this->subject->render($view)); self::assertTrue($this->logger->hasCriticalThatPasses(function ($logRecord) { /** @var Src\Exception\TemplatePathIsNotResolvable $exception */ $exception = $logRecord['context']['exception']; @@ -74,17 +120,22 @@ public function renderLogsCriticalErrorIfGivenTemplateIsNotAvailable(): void } #[Framework\Attributes\Test] - public function renderLogsCriticalErrorIfGivenTemplateIsNotReadable(): void + public function renderLogsCriticalErrorIfGivenViewIsNotProperlyInitialized(): void { - $this->templateResolver = new Tests\Unit\Fixtures\Classes\Renderer\Template\DummyTemplateResolver(); - $this->suppressNextError(E_WARNING); - self::assertSame('', $this->renewSubject()->render('foo.baz')); + $view = new Src\Renderer\Template\View\HandlebarsView(); + + self::assertSame('', $this->subject->render($view)); self::assertTrue($this->logger->hasCriticalThatPasses(function ($logRecord) { - /** @var Src\Exception\InvalidTemplateFileException $exception */ + /** @var Src\Exception\ViewIsNotProperlyInitialized $exception */ $exception = $logRecord['context']['exception']; - self::assertInstanceOf(Src\Exception\InvalidTemplateFileException::class, $exception); - self::assertSame(1606217313, $exception->getCode()); - self::assertStringEndsWith('foo.baz', $exception->getTemplateFile()); + + self::assertInstanceOf(Src\Exception\ViewIsNotProperlyInitialized::class, $exception); + self::assertSame( + 'The Handlebars view is not properly initialized. Provide either template path or template source.', + $exception->getMessage(), + ); + self::assertSame(1736332788, $exception->getCode()); + return true; })); } @@ -92,13 +143,17 @@ public function renderLogsCriticalErrorIfGivenTemplateIsNotReadable(): void #[Framework\Attributes\Test] public function renderUsesGivenTemplatePathIfItIsNotAvailableWithinTemplateRootPaths(): void { - self::assertSame('', $this->subject->render($this->templateRootPath . '/DummyTemplateEmpty')); + $view = new Src\Renderer\Template\View\HandlebarsView($this->templateRootPath . '/DummyTemplateEmpty'); + + self::assertSame('', $this->subject->render($view)); } #[Framework\Attributes\Test] public function renderReturnsEmptyStringIfGivenTemplateIsEmpty(): void { - self::assertSame('', $this->subject->render('DummyTemplateEmpty')); + $view = new Src\Renderer\Template\View\HandlebarsView('DummyTemplateEmpty'); + + self::assertSame('', $this->subject->render($view)); } #[Framework\Attributes\Test] @@ -115,9 +170,11 @@ public function renderMergesVariablesWithGivenVariables(): void another => "foo" (3 chars) EOF; + $view = new Src\Renderer\Template\View\HandlebarsView('DummyTemplateVariables', ['another' => 'foo']); + self::assertSame( \trim($expected), - \trim($this->subject->render('DummyTemplateVariables', ['another' => 'foo'])), + \trim($this->subject->render($view)), ); Core\Utility\DebugUtility::useAnsiColor(true); @@ -135,45 +192,36 @@ public function renderUsesCachedCompileResult(): void ); $this->assertCacheIsNotEmptyForTemplate('DummyTemplate.hbs'); - self::assertSame('foo', $this->subject->render('DummyTemplate')); - } + $view = new Src\Renderer\Template\View\HandlebarsView('DummyTemplate'); - #[Framework\Attributes\Test] - public function renderLogsCriticalErrorIfTemplateCompilationFails(): void - { - self::assertSame( - '', - $this->renewSubject(Tests\Unit\Fixtures\Classes\Renderer\DummyRenderer::class)->render('DummyTemplateErroneous') - ); - self::assertTrue($this->logger->hasCriticalThatPasses(function ($logRecord) { - /** @var Src\Exception\TemplateCompilationException $exception */ - $exception = $logRecord['context']['exception']; - self::assertInstanceOf(Src\Exception\TemplateCompilationException::class, $exception); - self::assertSame(1614620212, $exception->getCode()); - return true; - })); + self::assertSame('foo', $this->subject->render($view)); } #[Framework\Attributes\Test] public function renderDoesNotStoreRenderedTemplateInCacheIfDebugModeIsEnabled(): void { + $view = new Src\Renderer\Template\View\HandlebarsView('DummyTemplate'); + // Test with TypoScript config.debug = 1 $this->tsfeMock->config = ['config' => ['debug' => '1']]; - $this->renewSubject()->render('DummyTemplate'); + $this->renewSubject()->render($view); $this->assertCacheIsEmptyForTemplate('DummyTemplate.hbs'); // Test with TYPO3_CONF_VARS $this->tsfeMock->config = []; $GLOBALS['TYPO3_CONF_VARS']['FE']['debug'] = 1; - $this->renewSubject()->render('DummyTemplate'); + $this->renewSubject()->render($view); $this->assertCacheIsEmptyForTemplate('DummyTemplate.hbs'); } #[Framework\Attributes\Test] public function renderDoesNotStoreRenderedTemplateInCacheIfCachingIsDisabled(): void { + $view = new Src\Renderer\Template\View\HandlebarsView('DummyTemplate'); + $this->tsfeMock->no_cache = true; - $this->subject->render('DummyTemplate'); + $this->subject->render($view); + $this->assertCacheIsEmptyForTemplate('DummyTemplate.hbs'); } @@ -189,7 +237,9 @@ public function renderLogsCriticalErrorIfRenderingClosurePreparationFails(): voi ); $this->assertCacheIsNotEmptyForTemplate('DummyTemplate.hbs'); - self::assertSame('', $this->subject->render('DummyTemplate')); + $view = new Src\Renderer\Template\View\HandlebarsView('DummyTemplate'); + + self::assertSame('', $this->subject->render($view)); self::assertTrue($this->logger->hasCriticalThatPasses(function ($logRecord) { /** @var Src\Exception\TemplateCompilationException $exception */ $exception = $logRecord['context']['exception']; @@ -203,18 +253,22 @@ public function renderLogsCriticalErrorIfRenderingClosurePreparationFails(): voi #[Framework\Attributes\Test] public function renderReturnsRenderedTemplate(): void { + $view = new Src\Renderer\Template\View\HandlebarsView('DummyTemplate', ['name' => 'foo']); + self::assertSame( 'Hello, foo!', - \trim($this->subject->render('DummyTemplate', ['name' => 'foo'])) + \trim($this->subject->render($view)), ); } #[Framework\Attributes\Test] public function renderResolvesPartialsCorrectlyUsingPartialResolver(): void { + $view = new Src\Renderer\Template\View\HandlebarsView('DummyTemplateWithPartial', ['name' => 'foo']); + self::assertSame( 'Hello, foo!' . PHP_EOL . 'Welcome, foo, I am the partial!', - \trim($this->subject->render('DummyTemplateWithPartial', ['name' => 'foo'])) + \trim($this->subject->render($view)), ); } diff --git a/Tests/Unit/Renderer/Template/HandlebarsTemplateResolverTest.php b/Tests/Unit/Renderer/Template/HandlebarsTemplateResolverTest.php index d52af275..237c4b61 100644 --- a/Tests/Unit/Renderer/Template/HandlebarsTemplateResolverTest.php +++ b/Tests/Unit/Renderer/Template/HandlebarsTemplateResolverTest.php @@ -106,6 +106,24 @@ public function constructorThrowsExceptionIfFileExtensionIsInvalid(): void new Src\Renderer\Template\HandlebarsTemplateResolver($this->getTemplatePaths(), ['foo?!']); } + #[Framework\Attributes\Test] + public function resolvePartialPathThrowsExceptionIfGivenFormatIsNotSupported(): void + { + $this->expectExceptionObject( + new Src\Exception\TemplateFormatIsNotSupported('baz'), + ); + + $this->subject->resolvePartialPath('DummyPartial', 'baz'); + } + + #[Framework\Attributes\Test] + public function resolvePartialPathRespectsFormat(): void + { + $expected = $this->partialRootPath . '/DummyPartial.html'; + + self::assertSame($expected, $this->subject->resolvePartialPath('DummyPartial', 'html')); + } + #[Framework\Attributes\Test] public function resolvePartialPathThrowsExceptionIfPartialPathCannotBeResolved(): void { @@ -133,6 +151,24 @@ public function resolvePartialPathResolvesAbsolutePartialPathCorrectly(): void self::assertSame($expected, $this->subject->resolvePartialPath($templatePath)); } + #[Framework\Attributes\Test] + public function resolveTemplatePathThrowsExceptionIfGivenFormatIsNotSupported(): void + { + $this->expectExceptionObject( + new Src\Exception\TemplateFormatIsNotSupported('baz'), + ); + + $this->subject->resolveTemplatePath('DummyTemplate', 'baz'); + } + + #[Framework\Attributes\Test] + public function resolveTemplatePathRespectsFormat(): void + { + $expected = $this->templateRootPath . '/DummyTemplate.html'; + + self::assertSame($expected, $this->subject->resolveTemplatePath('DummyTemplate', 'html')); + } + #[Framework\Attributes\Test] public function resolveTemplatePathThrowsExceptionIfTemplatePathCannotBeResolved(): void { diff --git a/Tests/Unit/Renderer/Template/View/HandlebarsViewTest.php b/Tests/Unit/Renderer/Template/View/HandlebarsViewTest.php new file mode 100644 index 00000000..7aa46d9f --- /dev/null +++ b/Tests/Unit/Renderer/Template/View/HandlebarsViewTest.php @@ -0,0 +1,170 @@ + + * + * 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\Renderer\Template\View; + +use Fr\Typo3Handlebars as Src; +use Fr\Typo3Handlebars\Tests; +use PHPUnit\Framework; +use TYPO3\TestingFramework; + +/** + * HandlebarsViewTest + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +#[Framework\Attributes\CoversClass(Src\Renderer\Template\View\HandlebarsView::class)] +final class HandlebarsViewTest extends TestingFramework\Core\Unit\UnitTestCase +{ + use Tests\HandlebarsTemplateResolverTrait; + + private Src\Renderer\Template\View\HandlebarsView $subject; + + public function setUp(): void + { + parent::setUp(); + + $this->subject = new Src\Renderer\Template\View\HandlebarsView( + 'DummyTemplate', + [ + 'foo' => 'baz', + ], + ); + } + + #[Framework\Attributes\Test] + public function getTemplateThrowsExceptionIfNeitherTemplatePathNorTemplateSourceAreDefined(): void + { + $subject = new Src\Renderer\Template\View\HandlebarsView(); + + $this->expectExceptionObject( + new Src\Exception\ViewIsNotProperlyInitialized(), + ); + + $subject->getTemplate(); + } + + #[Framework\Attributes\Test] + public function getTemplateReturnsTemplateSourceIfDefined(): void + { + $this->subject->setTemplateSource('baz'); + + self::assertSame('baz', $this->subject->getTemplate()); + } + + #[Framework\Attributes\Test] + public function getTemplateThrowsExceptionIfTemplatePathIsInvalid(): void + { + $this->expectExceptionObject( + new Src\Exception\TemplateFileIsInvalid('DummyTemplate'), + ); + + $this->subject->getTemplate(); + } + + #[Framework\Attributes\Test] + public function getTemplateThrowsExceptionIfTemplateFileCannotBeRead(): void + { + $this->expectException(Src\Exception\TemplateFileIsInvalid::class); + + $this->subject->getTemplate(new Tests\Unit\Fixtures\Classes\Renderer\Template\DummyTemplateResolver()); + } + + #[Framework\Attributes\Test] + public function getTemplateReturnsTemplateFromResolvedTemplatePath(): void + { + self::assertStringEqualsFile( + $this->templateRootPath . '/DummyTemplate.hbs', + $this->subject->getTemplate($this->getTemplateResolver()), + ); + } + + #[Framework\Attributes\Test] + public function getTemplateReturnsTemplateFromResolvedTemplatePathWithConfiguredFormat(): void + { + $this->subject->setFormat('html'); + + self::assertStringEqualsFile( + $this->templateRootPath . '/DummyTemplate.html', + $this->subject->getTemplate($this->getTemplateResolver()), + ); + } + + #[Framework\Attributes\Test] + public function getTemplateReturnsTemplateFromConfiguredTemplatePath(): void + { + $templatePath = $this->templateRootPath . '/DummyTemplate.hbs'; + + $this->subject->setTemplatePath($templatePath); + + self::assertStringEqualsFile( + $templatePath, + $this->subject->getTemplate(), + ); + } + + #[Framework\Attributes\Test] + public function getTemplateReturnsTemplateFromConfiguredTemplatePathAndFormat(): void + { + $templatePath = $this->templateRootPath . '/DummyTemplate'; + + $this->subject->setTemplatePath($templatePath); + $this->subject->setFormat('html'); + + self::assertStringEqualsFile( + $templatePath . '.html', + $this->subject->getTemplate(), + ); + } + + #[Framework\Attributes\Test] + public function assignAddsGivenVariableToConfiguredVariables(): void + { + $expected = [ + 'foo' => 'baz', + 'baz' => 'foo', + ]; + + $this->subject->assign('baz', 'foo'); + + self::assertSame($expected, $this->subject->getVariables()); + } + + #[Framework\Attributes\Test] + public function assignMultipleAddsAllGivenVariablesToConfiguredVariables(): void + { + $expected = [ + 'foo' => 'baz', + 'baz' => 'foo', + 'boo' => 'foo', + ]; + + $this->subject->assignMultiple([ + 'baz' => 'foo', + 'boo' => 'foo', + ]); + + self::assertSame($expected, $this->subject->getVariables()); + } +}