1<?php
2namespace TYPO3Fluid\Fluid\Core\Cache;
3
4/*
5 * This file belongs to the package "TYPO3 Fluid".
6 * See LICENSE.txt that was shipped with this package.
7 */
8
9use TYPO3Fluid\Fluid\Core\Compiler\FailedCompilingState;
10use TYPO3Fluid\Fluid\Core\Compiler\StopCompilingException;
11use TYPO3Fluid\Fluid\Core\Parser\ParsedTemplateInterface;
12use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\ExpressionException;
13use TYPO3Fluid\Fluid\Core\Parser\TemplateParser;
14use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
15use TYPO3Fluid\Fluid\View\Exception;
16use TYPO3Fluid\Fluid\View\TemplatePaths;
17
18/**
19 * Class StandardCacheWarmer
20 *
21 * Responsible for performing a full warmup process.
22 * Receives just the RenderingContext (which can be custom for the
23 * framework that invokes the warmup) and resolves all possible
24 * template files in all supported formats and triggers compiling
25 * of those templates.
26 *
27 * The compiling process can be supported in detail in templates
28 * directly through using the `f:cache.*` collection of ViewHelpers.
29 * The compiler is put into a special warmup mode which can in turn
30 * be checked by ViewHelpers when compiling which allows third-party
31 * ViewHelpers to more closely control how they are compiled, if
32 * they are at all compilable.
33 *
34 * The result of the warmup process is returned as a
35 * FluidCacheWarmupResult instance with reports for every template
36 * file that was detected duringthe process; detailing whether or
37 * not the template file was compiled, some metadata about the
38 * template such as which Layout it uses, if any, and finally adds
39 * mitigation suggestions when a template cannot be compiled.
40 *
41 * The mitigation suggestions are specifically generated by this
42 * class and can be elaborated or changed completely by any third-
43 * party implementation of FluidCacheWarmerInterface which allows
44 * them to be specific to the framework in which Fluid is used.
45 * The default set of mitigation suggestions are based on the
46 * standard errors which can be thrown by the Fluid engine.
47 */
48class StandardCacheWarmer implements FluidCacheWarmerInterface
49{
50
51    /**
52     * Template file formats (file extensions) supported by this
53     * cache warmer implementation.
54     *
55     * @var array
56     */
57    protected $formats = ['html', 'xml', 'txt', 'json', 'rtf', 'atom', 'rss'];
58
59    /**
60     * Warm up an entire collection of templates based on the
61     * provided RenderingContext (the TemplatePaths carried by
62     * the RenderingContext, to be precise).
63     *
64     * Returns a FluidCacheWarmupResult with result information
65     * about all detected template files and the compiling of
66     * those files. If a template fails to compile or throws an
67     * error, a mitigation suggestion is included for that file.
68     *
69     * @param RenderingContextInterface $renderingContext
70     * @return FluidCacheWarmupResult
71     */
72    public function warm(RenderingContextInterface $renderingContext)
73    {
74        $renderingContext->getTemplateCompiler()->enterWarmupMode();
75        $result = new FluidCacheWarmupResult();
76        $result->merge(
77            $this->warmupTemplateRootPaths($renderingContext),
78            $this->warmupPartialRootPaths($renderingContext),
79            $this->warmupLayoutRootPaths($renderingContext)
80        );
81        return $result;
82    }
83
84    /**
85     * Warm up _templateRootPaths_ of the RenderingContext's
86     * TemplatePaths instance.
87     *
88     * Scans for template files recursively in all template root
89     * paths while respecting overlays, e.g. if a path replaces
90     * the template file of a lower priority path then only
91     * one result is returned - the overlayed template file. In
92     * other words the resolving happens exactly as if you were
93     * attempting to render each detected controller, so that the
94     * compiled template will be the same that is resolved when
95     * rendering that controller.
96     *
97     * Also scans the root level of all templateRootPaths for
98     * controller-less/fallback-action template files, e.g. files
99     * which would be rendered if a specified controller's action
100     * template does not exist (fallback-action) or if no controller
101     * name was specified in the context (controller-less).
102     *
103     * Like other methods, returns a FluidCacheWarmupResult instance
104     * which can be merged with other result instances.
105     *
106     * @param RenderingContextInterface $renderingContext
107     * @return FluidCacheWarmupResult
108     */
109    protected function warmupTemplateRootPaths(RenderingContextInterface $renderingContext)
110    {
111        $result = new FluidCacheWarmupResult();
112        $paths = $renderingContext->getTemplatePaths();
113        foreach ($this->formats as $format) {
114            $paths->setFormat($format);
115            $formatCutoffPoint = - (strlen($format) + 1);
116            foreach ($paths->getTemplateRootPaths() as $templateRootPath) {
117                $pathCutoffPoint = strlen($templateRootPath);
118                foreach ($this->detectControllerNamesInTemplateRootPaths([$templateRootPath]) as $controllerName) {
119                    foreach ($paths->resolveAvailableTemplateFiles($controllerName, $format) as $templateFile) {
120                        $state = $this->warmSingleFile(
121                            $templateFile,
122                            $paths->getTemplateIdentifier(
123                                $controllerName,
124                                substr($templateFile, $pathCutoffPoint, $formatCutoffPoint)
125                            ),
126                            $renderingContext
127                        );
128                        $result->add($state, $templateFile);
129                    }
130                }
131                $limitedPaths = clone $paths;
132                $limitedPaths->setTemplateRootPaths([$templateRootPath]);
133                foreach ($limitedPaths->resolveAvailableTemplateFiles(null, $format) as $templateFile) {
134                    $state = $this->warmSingleFile(
135                        $templateFile,
136                        $paths->getTemplateIdentifier(
137                            'Default',
138                            substr($templateFile, $pathCutoffPoint, $formatCutoffPoint)
139                        ),
140                        $renderingContext
141                    );
142                    $result->add($state, $templateFile);
143                }
144            }
145        }
146        return $result;
147    }
148
149    /**
150     * Warm up _partialRootPaths_ of the provided RenderingContext's
151     * TemplatePaths instance. Simple, recursive processing of all
152     * supported format template files in path(s), compiling only
153     * the topmost (override) template file if the same template
154     * exists in multiple partial root paths.
155     *
156     * Like other methods, returns a FluidCacheWarmupResult instance
157     * which can be merged with other result instances.
158     *
159     * @param RenderingContextInterface $renderingContext
160     * @return FluidCacheWarmupResult
161     */
162    protected function warmupPartialRootPaths(RenderingContextInterface $renderingContext)
163    {
164        $result = new FluidCacheWarmupResult();
165        $paths = $renderingContext->getTemplatePaths();
166        foreach ($this->formats as $format) {
167            $formatCutoffPoint = - (strlen($format) + 1);
168            foreach ($paths->getPartialRootPaths() as $partialRootPath) {
169                $limitedPaths = clone $paths;
170                $limitedPaths->setPartialRootPaths([$partialRootPath]);
171                $pathCutoffPoint = strlen($partialRootPath);
172                foreach ($limitedPaths->resolveAvailablePartialFiles($format) as $partialFile) {
173                    $paths->setFormat($format);
174                    $state = $this->warmSingleFile(
175                        $partialFile,
176                        $paths->getPartialIdentifier(substr($partialFile, $pathCutoffPoint, $formatCutoffPoint)),
177                        $renderingContext
178                    );
179                    $result->add($state, $partialFile);
180                }
181            }
182        }
183        return $result;
184    }
185
186    /**
187     * Warm up _layoutRootPaths_ of the provided RenderingContext's
188     * TemplatePaths instance. Simple, recursive processing of all
189     * supported format template files in path(s), compiling only
190     * the topmost (override) template file if the same template
191     * exists in multiple layout root paths.
192     *
193     * Like other methods, returns a FluidCacheWarmupResult instance
194     * which can be merged with other result instances.
195     *
196     * @param RenderingContextInterface $renderingContext
197     * @return FluidCacheWarmupResult
198     */
199    protected function warmupLayoutRootPaths(RenderingContextInterface $renderingContext)
200    {
201        $result = new FluidCacheWarmupResult();
202        $paths = $renderingContext->getTemplatePaths();
203        foreach ($this->formats as $format) {
204            $formatCutoffPoint = - (strlen($format) + 1);
205            foreach ($paths->getLayoutRootPaths() as $layoutRootPath) {
206                $limitedPaths = clone $paths;
207                $limitedPaths->setLayoutRootPaths([$layoutRootPath]);
208                $pathCutoffPoint = strlen($layoutRootPath);
209                foreach ($limitedPaths->resolveAvailableLayoutFiles($format) as $layoutFile) {
210                    $paths->setFormat($format);
211                    $state = $this->warmSingleFile(
212                        $layoutFile,
213                        $paths->getLayoutIdentifier(substr($layoutFile, $pathCutoffPoint, $formatCutoffPoint)),
214                        $renderingContext
215                    );
216                    $result->add($state, $layoutFile);
217                }
218            }
219        }
220        return $result;
221    }
222
223    /**
224     * Detect all available controller names in provided TemplateRootPaths
225     * array, returning the "basename" components of controller-template
226     * directories encountered, as an array.
227     *
228     * @param array $templateRootPaths
229     * @return \Generator
230     */
231    protected function detectControllerNamesInTemplateRootPaths(array $templateRootPaths)
232    {
233        foreach ($templateRootPaths as $templateRootPath) {
234            foreach ((array) glob(rtrim($templateRootPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . '*') as $pathName) {
235                if (is_dir($pathName)) {
236                    yield basename($pathName);
237                }
238            }
239        }
240    }
241
242    /**
243     * Warm up a single template file.
244     *
245     * Performs reading, parsing and attempts compiling of a single
246     * template file. Catches errors that may occur and reports them
247     * in a FailedCompilingState (which can then be `add()`'ed to
248     * the FluidCacheWarmupResult to assimilate the information within.
249     *
250     * Adds basic mitigation suggestions for each specific type of error,
251     * giving hints to developers if a certain template fails to compile.
252     *
253     * @param string $templatePathAndFilename
254     * @param string $identifier
255     * @param RenderingContextInterface $renderingContext
256     * @return ParsedTemplateInterface
257     */
258    protected function warmSingleFile($templatePathAndFilename, $identifier, RenderingContextInterface $renderingContext)
259    {
260        $parsedTemplate = new FailedCompilingState();
261        $parsedTemplate->setVariableProvider($renderingContext->getVariableProvider());
262        $parsedTemplate->setCompilable(false);
263        $parsedTemplate->setIdentifier($identifier);
264        try {
265            $parsedTemplate = $renderingContext->getTemplateParser()->getOrParseAndStoreTemplate(
266                $identifier,
267                $this->createClosure($templatePathAndFilename)
268            );
269        } catch (StopCompilingException $error) {
270            $parsedTemplate->setFailureReason(sprintf('Compiling is intentionally disabled. Specific reason unknown. Message: "%s"', $error->getMessage()));
271            $parsedTemplate->setMitigations([
272                'Can be caused by specific ViewHelpers. If this is is not intentional: avoid ViewHelpers which disable caches.',
273                'If cache is intentionally disabled: consider using `f:cache.static` to cause otherwise uncompilable ViewHelpers\' output to be replaced with a static string in compiled templates.'
274            ]);
275        } catch (ExpressionException $error) {
276            $parsedTemplate->setFailureReason(sprintf('ExpressionNode evaluation error: %s', $error->getMessage()));
277            $parsedTemplate->setMitigations([
278                'Emulate variables used in ExpressionNode using `f:cache.warmup` or assign in warming RenderingContext'
279            ]);
280        } catch (\TYPO3Fluid\Fluid\Core\Parser\Exception $error) {
281            $parsedTemplate->setFailureReason($error->getMessage());
282            $parsedTemplate->setMitigations([
283                'Fix possible syntax errors.',
284                'Check that all ViewHelpers are correctly referenced and namespaces loaded (note: namespaces may be added externally!)',
285                'Check that all ExpressionNode types used by the template are loaded (note: may depend on RenderingContext implementation!)',
286                'Emulate missing variables used in expressions by using `f:cache.warmup` around your template code.'
287            ]);
288        } catch (\TYPO3Fluid\Fluid\Core\ViewHelper\Exception $error) {
289            $parsedTemplate->setFailureReason(sprintf('ViewHelper threw Exception: %s', $error->getMessage()));
290            $parsedTemplate->setMitigations([
291                'Emulate missing variables using `f:cache.warmup` around failing ViewHelper.',
292                'Emulate globals / context required by ViewHelper.',
293                'Disable caching for template if ViewHelper depends on globals / context that cannot be emulated.'
294            ]);
295        } catch (\TYPO3Fluid\Fluid\Core\Exception $error) {
296            $parsedTemplate->setFailureReason(sprintf('Fluid engine error: %s', $error->getMessage()));
297            $parsedTemplate->setMitigations([
298                'Search online for additional information about specific error.'
299            ]);
300        } catch (Exception $error) {
301            $parsedTemplate->setFailureReason(sprintf('Fluid view error: %s', $error->getMessage()));
302            $parsedTemplate->setMitigations([
303                'Investigate reported error in View class for missing variable checks, missing configuration etc.',
304                'Consider using a different View class for rendering in warmup mode (a custom rendering context can provide it)'
305            ]);
306        } catch (\RuntimeException $error) {
307            $parsedTemplate->setFailureReason(
308                sprintf(
309                    'General error: %s line %s threw %s (code: %d)',
310                    get_class($error),
311                    $error->getFile(),
312                    $error->getLine(),
313                    $error->getMessage(),
314                    $error->getCode()
315                )
316            );
317            $parsedTemplate->setMitigations([
318                'There are no automated suggestions for mitigating this issue. An online search may yield more information.'
319            ]);
320        }
321        return $parsedTemplate;
322    }
323
324    /**
325     * @param string $templatePathAndFilename
326     * @return \Closure
327     */
328    protected function createClosure($templatePathAndFilename)
329    {
330        return function(TemplateParser $parser, TemplatePaths $templatePaths) use ($templatePathAndFilename) {
331            return file_get_contents($templatePathAndFilename, FILE_TEXT);
332        };
333    }
334}
335