Skip to content

Commit ee5b89d

Browse files
authored
[FEATURE] Overhauled cache warmup command (#1224)
Previous Fluid versions already had a CLI command which could be used to warmup the cache for template files, e. g. during deployment. However, the previous implementation required a lot of additional context and mixed parsing/compilation and rendering state. The previous warmup implementation has already been deprecated with Fluid v4 and removed from Fluid v5 and is now replaced with a more straightforward implementation. The new CLI command needs to be supplied with a list of paths that should be scanned for template files as well as the location of the template cache. By default, the command uses the newly introduced Fluid file extension (see #1258) to identify Fluid templates. However, optionally an alternative file extension can be supplied as well. ``` bin/fluid warmup \ --cacheDirectory examples/cache/ \ --path examples/Resources/ bin/fluid warmup \ --cacheDirectory examples/cache/ \ --path examples/Resources/ \ --path examples/ResourceOverrides/ bin/fluid warmup \ --cacheDirectory examples/cache/ \ --path examples/Resources/ \ --extension html ``` In addition to the cache warmup, the command collects and outputs exceptions and deprecations that happened during parsing and compilation of the found template files. The underlying API, namely `TemplateFinder` for finding template files and `TemplateValidator`, which produces a validation report for each template, are marked as `@internal` for now. Resolves: #1103
1 parent 31a451f commit ee5b89d

15 files changed

+426
-34
lines changed

src/Tools/ConsoleRunner.php

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@
1212
use Composer\Autoload\ClassLoader;
1313
use TYPO3Fluid\Fluid\Core\Cache\SimpleFileCache;
1414
use TYPO3Fluid\Fluid\Core\Parser\Patterns;
15+
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContext;
1516
use TYPO3Fluid\Fluid\Core\Variables\JSONVariableProvider;
1617
use TYPO3Fluid\Fluid\Core\Variables\StandardVariableProvider;
1718
use TYPO3Fluid\Fluid\Core\Variables\VariableProviderInterface;
1819
use TYPO3Fluid\Fluid\Exception;
1920
use TYPO3Fluid\Fluid\Schema\SchemaGenerator;
2021
use TYPO3Fluid\Fluid\Schema\ViewHelperFinder;
22+
use TYPO3Fluid\Fluid\Validation\TemplateValidator;
2123
use TYPO3Fluid\Fluid\View\AbstractTemplateView;
24+
use TYPO3Fluid\Fluid\View\TemplateFinder;
2225
use TYPO3Fluid\Fluid\View\TemplatePaths;
2326
use TYPO3Fluid\Fluid\View\TemplateView;
2427

@@ -30,6 +33,7 @@ final class ConsoleRunner
3033
private const COMMAND_HELP = 'help';
3134
private const COMMAND_RUN = 'run';
3235
private const COMMAND_SCHEMA = 'schema';
36+
private const COMMAND_WARMUP = 'warmup';
3337

3438
private const ARGUMENT_HELP = 'help';
3539
private const ARGUMENT_SOCKET = 'socket';
@@ -44,11 +48,14 @@ final class ConsoleRunner
4448
private const ARGUMENT_PARTIALROOTPATHS = 'partialRootPaths';
4549
private const ARGUMENT_RENDERINGCONTEXT = 'renderingContext';
4650
private const ARGUMENT_DESTINATION = 'destination';
51+
private const ARGUMENT_PATH = 'path';
52+
private const ARGUMENT_EXTENSION = 'extension';
4753

4854
private array $commandDesccriptions = [
4955
self::COMMAND_HELP => 'Show this help screen',
5056
self::COMMAND_RUN => 'Run fluid code, either interactively or file-based',
5157
self::COMMAND_SCHEMA => 'Generate xsd schema files based on all available ViewHelper classes',
58+
self::COMMAND_WARMUP => 'Warmup template cache',
5259
];
5360

5461
private array $argumentDescriptions = [
@@ -72,6 +79,11 @@ final class ConsoleRunner
7279
self::ARGUMENT_HELP => 'Shows usage examples',
7380
self::ARGUMENT_DESTINATION => 'Destination folder where the schema files should be written to',
7481
],
82+
self::COMMAND_WARMUP => [
83+
self::ARGUMENT_PATH => 'Paths that should be checked for template files for warmup',
84+
self::ARGUMENT_CACHEDIRECTORY => 'Path to a directory used as cache for compiled Fluid templates',
85+
self::ARGUMENT_EXTENSION => 'File extensions that should be treated as Fluid templates (default: *.fluid.*)',
86+
],
7587
];
7688

7789
/**
@@ -102,6 +114,9 @@ public function handleCommand(array $arguments, ClassLoader $autoloader): string
102114
case self::COMMAND_SCHEMA:
103115
return $this->handleSchemaCommand($arguments, $autoloader);
104116

117+
case self::COMMAND_WARMUP:
118+
return $this->handleWarmupCommand($arguments);
119+
105120
case self::COMMAND_RUN:
106121
default:
107122
return $this->handleRunCommand($arguments);
@@ -151,6 +166,86 @@ private function handleSchemaCommand(array $arguments, ClassLoader $autoloader):
151166
return '';
152167
}
153168

169+
private function handleWarmupCommand(array $arguments): string
170+
{
171+
// @todo add argument for global namespaces
172+
$paths = $arguments[self::ARGUMENT_PATH] ?? [];
173+
$cacheDirectory = $arguments[self::ARGUMENT_CACHEDIRECTORY] ?? '';
174+
$extension = $arguments[self::ARGUMENT_EXTENSION] ?? null;
175+
if ($paths === []) {
176+
throw new \InvalidArgumentException(
177+
'At least one path needs to be supplied to perform cache warmup.',
178+
);
179+
}
180+
if ($cacheDirectory === '') {
181+
throw new \InvalidArgumentException(
182+
'Cache directory needs to be supplied to perform cache warmup.',
183+
);
184+
}
185+
186+
$templateScanner = new TemplateFinder();
187+
$templates = $extension === null
188+
? $templateScanner->findTemplatesWithFluidFileExtension($paths)
189+
: $templateScanner->findTemplatesByFileExtension($paths, $extension);
190+
191+
$templateValidator = new TemplateValidator();
192+
$scanResults = $templateValidator->validateTemplateFiles($templates);
193+
194+
if (!is_dir($cacheDirectory)) {
195+
mkdir($cacheDirectory);
196+
}
197+
$cache = new SimpleFileCache($cacheDirectory);
198+
199+
$output = [];
200+
foreach ($scanResults as $result) {
201+
$errors = [];
202+
foreach ($result->errors as $error) {
203+
$errors[] = sprintf(
204+
'Parsing error triggered in %s, line %d: %s',
205+
$error->getFile(),
206+
$error->getLine(),
207+
$error->getMessage(),
208+
);
209+
}
210+
211+
foreach ($result->deprecations as $deprecation) {
212+
$errors[] = sprintf(
213+
'Deprecation triggered in %s, line %d: %s',
214+
$deprecation->file,
215+
$deprecation->line,
216+
$deprecation->message,
217+
);
218+
}
219+
220+
if ($result->canBeCompiled()) {
221+
try {
222+
$cachingRenderingContext = new RenderingContext();
223+
$cachingRenderingContext->setCache($cache);
224+
$cachingRenderingContext->getTemplateCompiler()->store(
225+
$result->identifier,
226+
$result->parsedTemplate,
227+
);
228+
} catch (\Exception $e) {
229+
$errors[] = sprintf(
230+
'Compilation error triggered in %s, line %s: %s',
231+
$e->getFile(),
232+
$e->getLine(),
233+
$e->getMessage(),
234+
);
235+
}
236+
} else {
237+
$errors[] = 'Template cannot be compiled.';
238+
}
239+
240+
if ($errors !== []) {
241+
$title = sprintf('Template %s (%s)', $result->path, $result->identifier);
242+
$output[] = implode(PHP_EOL . ' ', [$title, ...$errors]);
243+
}
244+
}
245+
246+
return implode(PHP_EOL . PHP_EOL, $output) . PHP_EOL;
247+
}
248+
154249
/**
155250
* @param array<string, string|array> $arguments
156251
*/
@@ -357,6 +452,9 @@ private function parseAndValidateInputArguments(array $arguments, array $allowed
357452
if (isset($parsed[self::ARGUMENT_PARTIALROOTPATHS])) {
358453
$parsed[self::ARGUMENT_PARTIALROOTPATHS] = (array)$parsed[self::ARGUMENT_PARTIALROOTPATHS];
359454
}
455+
if (isset($parsed[self::ARGUMENT_PATH])) {
456+
$parsed[self::ARGUMENT_PATH] = (array)$parsed[self::ARGUMENT_PATH];
457+
}
360458
return $parsed;
361459
}
362460

src/Validation/Deprecation.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file belongs to the package "TYPO3 Fluid".
7+
* See LICENSE.txt that was shipped with this package.
8+
*/
9+
10+
namespace TYPO3Fluid\Fluid\Validation;
11+
12+
/**
13+
* @internal
14+
*/
15+
final readonly class Deprecation
16+
{
17+
public function __construct(
18+
public string $file,
19+
public int $line,
20+
public string $message,
21+
) {}
22+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file belongs to the package "TYPO3 Fluid".
7+
* See LICENSE.txt that was shipped with this package.
8+
*/
9+
10+
namespace TYPO3Fluid\Fluid\Validation;
11+
12+
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContext;
13+
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
14+
15+
/**
16+
* @internal
17+
*/
18+
final readonly class TemplateValidator
19+
{
20+
/**
21+
* Collects deprecations and exceptions during parsing and compilation
22+
* of the supplied Fluid template files
23+
*
24+
* @param string[] $templates
25+
* @return TemplateValidatorResult[]
26+
*/
27+
public function validateTemplateFiles(array $templates, ?RenderingContextInterface $baseRenderingContext = null): array
28+
{
29+
$baseRenderingContext ??= new RenderingContext();
30+
$results = [];
31+
foreach ($templates as $template) {
32+
$deprecations = $errors = [];
33+
set_error_handler(
34+
function (int $errno, string $errstr, string $errfile, int $errline) use (&$deprecations): bool {
35+
$deprecations[] = new Deprecation($errfile, $errline, $errstr);
36+
return true;
37+
},
38+
E_USER_DEPRECATED,
39+
);
40+
41+
$renderingContext = clone $baseRenderingContext;
42+
$renderingContext->setViewHelperResolver($renderingContext->getViewHelperResolver()->getScopedCopy());
43+
$templatePaths = $renderingContext->getTemplatePaths();
44+
$templatePaths->setTemplatePathAndFilename($template);
45+
$templateIdentifier = $templatePaths->getTemplateIdentifier();
46+
$parsedTemplate = null;
47+
try {
48+
$parsedTemplate = $renderingContext->getTemplateParser()->parse(
49+
$templatePaths->getTemplateSource(),
50+
$templateIdentifier,
51+
);
52+
} catch (\Exception $e) {
53+
$errors[] = $e;
54+
}
55+
56+
restore_error_handler();
57+
$results[$template] = new TemplateValidatorResult(
58+
$templateIdentifier,
59+
$template,
60+
$errors,
61+
$deprecations,
62+
$parsedTemplate,
63+
);
64+
}
65+
return $results;
66+
}
67+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file belongs to the package "TYPO3 Fluid".
7+
* See LICENSE.txt that was shipped with this package.
8+
*/
9+
10+
namespace TYPO3Fluid\Fluid\Validation;
11+
12+
use TYPO3Fluid\Fluid\Core\Parser\ParsingState;
13+
14+
/**
15+
* @internal
16+
*/
17+
final readonly class TemplateValidatorResult
18+
{
19+
/**
20+
* @param \Exception[] $errors
21+
* @param Deprecation[] $deprecations
22+
*/
23+
public function __construct(
24+
public string $identifier,
25+
public string $path,
26+
public array $errors,
27+
public array $deprecations,
28+
public ?ParsingState $parsedTemplate,
29+
) {}
30+
31+
/**
32+
* Creates a copy with different errors. This allows
33+
* to attach errors after the object has been created,
34+
* e. g. errors happening during template compilation
35+
*
36+
* @param \Exception[] $errors
37+
*/
38+
public function withErrors(array $errors): self
39+
{
40+
return new self(
41+
identifier: $this->identifier,
42+
path: $this->path,
43+
errors: $errors,
44+
deprecations: $this->deprecations,
45+
parsedTemplate: $this->parsedTemplate,
46+
);
47+
}
48+
49+
public function canBeCompiled(): bool
50+
{
51+
return $this->errors === [] && $this->parsedTemplate?->isCompilable();
52+
}
53+
}

src/View/TemplateFinder.php

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file belongs to the package "TYPO3 Fluid".
7+
* See LICENSE.txt that was shipped with this package.
8+
*/
9+
10+
namespace TYPO3Fluid\Fluid\View;
11+
12+
use AppendIterator;
13+
use CallbackFilterIterator;
14+
use FilesystemIterator;
15+
use Iterator;
16+
use RecursiveDirectoryIterator;
17+
use RecursiveIteratorIterator;
18+
use SplFileInfo;
19+
20+
/**
21+
* @internal
22+
*/
23+
final readonly class TemplateFinder
24+
{
25+
/**
26+
* @param string[] $paths
27+
* @return string[]
28+
*/
29+
public function findTemplatesWithFluidFileExtension(array $paths): array
30+
{
31+
if ($paths === []) {
32+
return [];
33+
}
34+
$filterIterator = new CallbackFilterIterator(
35+
$this->createFileIterator($paths),
36+
fn(SplFileInfo $file): bool => str_contains($file->getBasename(), '.' . TemplatePaths::FLUID_EXTENSION . '.'),
37+
);
38+
return array_keys(iterator_to_array($filterIterator));
39+
}
40+
41+
/**
42+
* @param string[] $paths
43+
* @return string[]
44+
*/
45+
public function findTemplatesByFileExtension(array $paths, string $fileExtension): array
46+
{
47+
if ($paths === []) {
48+
return [];
49+
}
50+
$filterIterator = new CallbackFilterIterator(
51+
$this->createFileIterator($paths),
52+
fn(SplFileInfo $file): bool => str_ends_with($file->getBasename(), '.' . $fileExtension),
53+
);
54+
return array_keys(iterator_to_array($filterIterator));
55+
}
56+
57+
/**
58+
* @param string[] $paths
59+
*/
60+
private function createFileIterator(array $paths): Iterator
61+
{
62+
$appendIterator = new AppendIterator();
63+
foreach ($paths as $path) {
64+
$directoryIterator = new RecursiveDirectoryIterator($path, FilesystemIterator::FOLLOW_SYMLINKS | FilesystemIterator::SKIP_DOTS);
65+
$recursiveIterator = new RecursiveIteratorIterator($directoryIterator, RecursiveIteratorIterator::SELF_FIRST);
66+
$appendIterator->append($recursiveIterator);
67+
}
68+
return $appendIterator;
69+
}
70+
}

0 commit comments

Comments
 (0)