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); 332 }; 333 } 334} 335