‪TYPO3CMS  ‪main
FrontendTypoScriptFactory.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Psr\Container\ContainerInterface;
21 use Psr\EventDispatcher\EventDispatcherInterface;
22 use Psr\Http\Message\ServerRequestInterface;
39 
46 final readonly class FrontendTypoScriptFactory
47 {
48  public function __construct(
49  private ContainerInterface $container,
50  private EventDispatcherInterface $eventDispatcher,
51  private SysTemplateTreeBuilder $treeBuilder,
52  private LossyTokenizer $tokenizer,
53  private IncludeTreeTraverser $includeTreeTraverser,
54  private ConditionVerdictAwareIncludeTreeTraverser $includeTreeTraverserConditionVerdictAware,
55  ) {}
56 
69  public function createSettingsAndSetupConditions(
70  SiteInterface $site,
71  array $sysTemplateRows,
72  array $expressionMatcherVariables,
73  ?PhpFrontend $typoScriptCache,
74  ): FrontendTypoScript {
75  $settingsDetails = $this->createSettings(
76  $site,
77  $sysTemplateRows,
78  $expressionMatcherVariables,
79  $typoScriptCache
80  );
81  $setupDetails = $this->createSetupConditionList(
82  $site,
83  $sysTemplateRows,
84  $expressionMatcherVariables,
85  $typoScriptCache,
86  $settingsDetails['flatSettings'],
87  $settingsDetails['settingsConditionList'],
88  );
89  $frontendTypoScript = new FrontendTypoScript(
90  $settingsDetails['settingsTree'],
91  $settingsDetails['settingsConditionList'],
92  $settingsDetails['flatSettings'],
93  $setupDetails['setupConditionList'],
94  );
95  if ($setupDetails['setupIncludeTree']) {
96  $frontendTypoScript->setSetupIncludeTree($setupDetails['setupIncludeTree']);
97  }
98  return $frontendTypoScript;
99  }
100 
119  private function createSettings(
120  SiteInterface $site,
121  array $sysTemplateRows,
122  array $expressionMatcherVariables,
123  ?PhpFrontend $typoScriptCache,
124  ): array {
125  $conditionTreeCacheIdentifier = 'settings-condition-tree-' . hash('xxh3', json_encode($sysTemplateRows, JSON_THROW_ON_ERROR));
126 
127  if ($conditionTree = $typoScriptCache?->require($conditionTreeCacheIdentifier)) {
128  // Got the (flat) include tree of all settings conditions for this TypoScript combination from cache.
129  // Good. Traverse this list to calculate "current" condition verdicts. Hash this list together with a
130  // hash of the TypoScript sys_templates, and try to retrieve the full settings TypoScript AST from cache.
131  // Note: Working with the derived condition tree that *only* contains conditions, but not the full
132  // include tree is a trick: We only need the condition verdicts to know the AST cache identifier,
133  // and traversing the flat condition tree is quicker than traversing the entire settings include tree,
134  // since it only scales with the number of settings conditions and not with the full amount of TypoScript
135  // settings. The same trick is used for the setup AST cache later.
136  $conditionMatcherVisitor = $this->container->get(IncludeTreeConditionMatcherVisitor::class);
137  $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables);
138  // It does not matter if we use IncludeTreeTraverser or ConditionVerdictAwareIncludeTreeTraverser here:
139  // Conditions list is flat, not nested. IncludeTreeTraverser has an if() less, so we use that one.
140  $this->includeTreeTraverser->traverse($conditionTree, [$conditionMatcherVisitor]);
141  $conditionList = $conditionMatcherVisitor->getConditionListWithVerdicts();
142  $settings = $typoScriptCache->require(
143  'settings-' . hash('xxh3', $conditionTreeCacheIdentifier . json_encode($conditionList, JSON_THROW_ON_ERROR))
144  );
145  if (is_array($settings)) {
146  return [
147  'settingsTree' => $settings['ast'],
148  'flatSettings' => $settings['flatSettings'],
149  'settingsConditionList' => $conditionList,
150  ];
151  }
152  }
153 
154  // We did not get settings from cache, or are not allowed to use cache. Build settings from scratch.
155  // We fetch the full settings include tree (from cache if possible), register the condition
156  // matcher and register the AST builder and traverse include tree to retrieve settings AST and derive
157  // 'flat settings' from it. Both are cached if allowed afterward for the above 'if' to kick in next time.
158  $includeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, $this->tokenizer, $site, $typoScriptCache);
159  $conditionMatcherVisitor = $this->container->get(IncludeTreeConditionMatcherVisitor::class);
160  $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables);
161  $visitors = [];
162  $visitors[] = $conditionMatcherVisitor;
163  $astBuilderVisitor = $this->container->get(IncludeTreeAstBuilderVisitor::class);
164  $visitors[] = $astBuilderVisitor;
165  // We must use ConditionVerdictAwareIncludeTreeTraverser here: This one does not walk into
166  // children for not matching conditions, which is important to create the correct AST.
167  $this->includeTreeTraverserConditionVerdictAware->traverse($includeTree, $visitors);
168  $tree = $astBuilderVisitor->getAst();
169  // @internal Dispatch an experimental event allowing listeners to still change the settings AST,
170  // to for instance implement nested constants if really needed. Note this event may change
171  // or vanish later without further notice.
172  $tree = $this->eventDispatcher->dispatch(new ModifyTypoScriptConstantsEvent($tree))->getConstantsAst();
173  $flatSettings = $tree->flatten();
174 
175  // Prepare the full list of settings conditions in order to cache this list, avoiding the
176  // settings AST building next time. We need all conditions of the entire include tree, but the
177  // above ConditionVerdictAwareIncludeTreeTraverser did not find nested conditions if an upper
178  // condition did not match. We thus have to traverse include tree a second time with the
179  // IncludeTreeTraverser. This one does traverse into not matching conditions.
180  $visitors = [];
181  $conditionMatcherVisitor = $this->container->get(IncludeTreeConditionMatcherVisitor::class);
182  $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables);
183  $visitors[] = $conditionMatcherVisitor;
184  $conditionTreeAccumulatorVisitor = null;
185  if (!$conditionTree && $typoScriptCache) {
186  // If the settingsConditionTree did not come from cache above and if we are allowed to cache,
187  // register the visitor that creates the settings condition include tree, to cache it.
188  $conditionTreeAccumulatorVisitor = $this->container->get(IncludeTreeConditionIncludeListAccumulatorVisitor::class);
189  $visitors[] = $conditionTreeAccumulatorVisitor;
190  }
191  $this->includeTreeTraverser->traverse($includeTree, $visitors);
192  $conditionList = $conditionMatcherVisitor->getConditionListWithVerdicts();
193 
194  if ($conditionTreeAccumulatorVisitor) {
195  // Cache the flat condition include tree for next run.
196  $conditionTree = $conditionTreeAccumulatorVisitor->getConditionIncludes();
197  $typoScriptCache?->set(
198  $conditionTreeCacheIdentifier,
199  'return unserialize(\'' . addcslashes(serialize($conditionTree), '\'\\') . '\');'
200  );
201  }
202  $typoScriptCache?->set(
203  // Cache full AST and the derived 'flattened' variant for next run, which will kick in if
204  // the sys_templates and condition verdicts are identical with another Request.
205  'settings-' . hash('xxh3', $conditionTreeCacheIdentifier . json_encode($conditionList, JSON_THROW_ON_ERROR)),
206  'return unserialize(\'' . addcslashes(serialize(['ast' => $tree, 'flatSettings' => $flatSettings]), '\'\\') . '\');'
207  );
208 
209  return [
210  'settingsTree' => $tree,
211  'flatSettings' => $flatSettings,
212  'settingsConditionList' => $conditionList,
213  ];
214  }
215 
235  private function createSetupConditionList(
236  SiteInterface $site,
237  array $sysTemplateRows,
238  array $expressionMatcherVariables,
239  ?PhpFrontend $typoScriptCache,
240  array $flatSettings,
241  array $settingsConditionList,
242  ): array {
243  $conditionTreeCacheIdentifier = 'setup-condition-tree-'
244  . hash('xxh3', json_encode($sysTemplateRows, JSON_THROW_ON_ERROR) . json_encode($settingsConditionList, JSON_THROW_ON_ERROR));
245 
246  if ($conditionTree = $typoScriptCache?->require($conditionTreeCacheIdentifier)) {
247  // We got the flat list of all setup conditions for this TypoScript combination from cache. Good. We traverse
248  // this list to calculate "current" condition verdicts, which we need as hash to be part of page cache identifier.
249  // We're done and return. Note 'setupIncludeTree' is *not* returned in this case since it is not needed and
250  // may or may not be needed later, depending on if we can get a page cache entry later and if it has _INT objects.
251  $visitors = [];
252  $conditionConstantSubstitutionVisitor = $this->container->get(IncludeTreeSetupConditionConstantSubstitutionVisitor::class);
253  $conditionConstantSubstitutionVisitor->setFlattenedConstants($flatSettings);
254  $visitors[] = $conditionConstantSubstitutionVisitor;
255  $conditionMatcherVisitor = $this->container->get(IncludeTreeConditionMatcherVisitor::class);
256  $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables);
257  $visitors[] = $conditionMatcherVisitor;
258  // It does not matter if we use IncludeTreeTraverser or ConditionVerdictAwareIncludeTreeTraverser here:
259  // Condition list is flat, not nested. IncludeTreeTraverser has an if() less, so we use that one.
260  $this->includeTreeTraverser->traverse($conditionTree, $visitors);
261  return [
262  'setupConditionList' => $conditionMatcherVisitor->getConditionListWithVerdicts(),
263  'setupIncludeTree' => null,
264  ];
265  }
266 
267  // We did not get setup condition list from cache, or are not allowed to use cache. We have to build setup
268  // condition list from scratch. This means we'll fetch the full setup include tree (from cache if possible),
269  // register the constant substitution visitor, the condition matcher and the condition accumulator visitor.
270  $includeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $this->tokenizer, $site, $typoScriptCache);
271  $visitors = [];
272  $conditionConstantSubstitutionVisitor = $this->container->get(IncludeTreeSetupConditionConstantSubstitutionVisitor::class);
273  $conditionConstantSubstitutionVisitor->setFlattenedConstants($flatSettings);
274  $visitors[] = $conditionConstantSubstitutionVisitor;
275  $conditionMatcherVisitor = $this->container->get(IncludeTreeConditionMatcherVisitor::class);
276  $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables);
277  $visitors[] = $conditionMatcherVisitor;
278  $conditionTreeAccumulatorVisitor = $this->container->get(IncludeTreeConditionIncludeListAccumulatorVisitor::class);
279  $visitors[] = $conditionTreeAccumulatorVisitor;
280  // It is important to use IncludeTreeTraverser here: We need the condition verdicts of *all* conditions, and
281  // we want to accumulate all of them. The ConditionVerdictAwareIncludeTreeTraverser wouldn't walk into nested
282  // conditions if an upper one does not match, which defeats cache identifier calculations.
283  $this->includeTreeTraverser->traverse($includeTree, $visitors);
284 
285  $typoScriptCache?->set(
286  $conditionTreeCacheIdentifier,
287  'return unserialize(\'' . addcslashes(serialize($conditionTreeAccumulatorVisitor->getConditionIncludes()), '\'\\') . '\');'
288  );
289 
290  return [
291  'setupConditionList' => $conditionMatcherVisitor->getConditionListWithVerdicts(),
292  'setupIncludeTree' => $includeTree,
293  ];
294  }
295 
324  public function createSetupConfigOrFullSetup(
325  bool $needsFullSetup,
326  FrontendTypoScript $frontendTypoScript,
327  SiteInterface $site,
328  array $sysTemplateRows,
329  array $expressionMatcherVariables,
330  string $type,
331  ?PhpFrontend $typoScriptCache,
332  ?ServerRequestInterface $request,
333  ): FrontendTypoScript {
334  $setupTypoScriptCacheIdentifier = 'setup-' . hash(
335  'xxh3',
336  json_encode($sysTemplateRows, JSON_THROW_ON_ERROR)
337  . json_encode($frontendTypoScript->getSettingsConditionList(), JSON_THROW_ON_ERROR)
338  . json_encode($frontendTypoScript->getSetupConditionList(), JSON_THROW_ON_ERROR)
339  );
340  $setupConfigTypoScriptCacheIdentifier = 'setup-config-' . hash('xxh3', $setupTypoScriptCacheIdentifier . $type);
341 
342  $gotSetupConfigFromCache = false;
343  if ($setupConfigTypoScriptCache = $typoScriptCache?->require($setupConfigTypoScriptCacheIdentifier)) {
344  $frontendTypoScript->setConfigTree($setupConfigTypoScriptCache['ast']);
345  $frontendTypoScript->setConfigArray($setupConfigTypoScriptCache['array']);
346  if (!$needsFullSetup) {
347  // Fully cached page context without _INT - only 'config' is needed. Return early.
348  return $frontendTypoScript;
349  }
350  $gotSetupConfigFromCache = true;
351  }
352 
353  $setupRawConfigAst = null;
354  if (!$typoScriptCache || $needsFullSetup || !$gotSetupConfigFromCache) {
355  // If caching is not allowed, if no page cache entry could be loaded or if the page cache entry has _INT
356  // object, we need the full setup AST. Try to use a cache entry for setup AST, which especially up _INT
357  // parsing. In unavailable, calculate full setup AST and cache it if allowed.
358  $gotSetupFromCache = false;
359  if ($setupTypoScriptCache = $typoScriptCache?->require($setupTypoScriptCacheIdentifier)) {
360  // We need AST, and we got it from cache.
361  $frontendTypoScript->setSetupTree($setupTypoScriptCache['ast']);
362  $frontendTypoScript->setSetupArray($setupTypoScriptCache['array']);
363  $setupRawConfigAst = $setupTypoScriptCache['ast']->getChildByName('config');
364  $gotSetupFromCache = true;
365  }
366  if (!$typoScriptCache || !$gotSetupFromCache) {
367  // We need AST and couldn't get it from cache or are now allowed to. We thus need the full setup
368  // IncludeTree, which we can get from cache again if allowed, or is calculated a-new if not.
369  $setupIncludeTree = $frontendTypoScript->getSetupIncludeTree();
370  if (!$typoScriptCache || $setupIncludeTree === null) {
371  // A previous method *may* have calculated setup include tree already. Calculate now if not.
372  $setupIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $this->tokenizer, $site, $typoScriptCache);
373  }
374  $visitors = [];
375  $conditionConstantSubstitutionVisitor = $this->container->get(IncludeTreeSetupConditionConstantSubstitutionVisitor::class);
376  $conditionConstantSubstitutionVisitor->setFlattenedConstants($frontendTypoScript->getFlatSettings());
377  $visitors[] = $conditionConstantSubstitutionVisitor;
378  $conditionMatcherVisitor = $this->container->get(IncludeTreeConditionMatcherVisitor::class);
379  $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables);
380  $visitors[] = $conditionMatcherVisitor;
381  $astBuilderVisitor = $this->container->get(IncludeTreeAstBuilderVisitor::class);
382  $astBuilderVisitor->setFlatConstants($frontendTypoScript->getFlatSettings());
383  $visitors[] = $astBuilderVisitor;
384  $this->includeTreeTraverserConditionVerdictAware->traverse($setupIncludeTree, $visitors);
385  $setupAst = $astBuilderVisitor->getAst();
386  // @todo: It would be good to actively remove 'config' from AST and array here
387  // to prevent people from using the unmerged variant. The same
388  // is already done for the determined PAGE 'config' below. This works, but
389  // is currently blocked by functional tests that assert details?
390  // Also, we need to still cache with full 'config' to handle multiple types.
391  $setupRawConfigAst = $setupAst->getChildByName('config');
392  // $setupAst->removeChildByName('config');
393  $frontendTypoScript->setSetupTree($setupAst);
394  $frontendTypoScript->setSetupArray($setupAst->toArray());
395 
396  // Write cache entry for AST and its array representation.
397  $typoScriptCache?->set(
398  $setupTypoScriptCacheIdentifier,
399  'return unserialize(\'' . addcslashes(serialize(['ast' => $setupAst, 'array' => $setupAst->toArray()]), '\'\\') . '\');'
400  );
401  }
402 
403  $setupAst = $frontendTypoScript->getSetupTree();
404  $rawSetupPageNodeFromType = null;
405  $pageNodeFoundByType = false;
406  foreach ($setupAst->getNextChild() as $potentialPageNode) {
407  // Find the PAGE object that matches given type/typeNum
408  if ($potentialPageNode->getValue() === 'PAGE') {
409  // @todo: We could potentially remove *all* PAGE objects from setup here. This prevents people
410  // from accessing other ones than the determined one in $frontendTypoScript->getSetupArray().
411  $typeNumChild = $potentialPageNode->getChildByName('typeNum');
412  if ($typeNumChild && $type === $typeNumChild->getValue()) {
413  $rawSetupPageNodeFromType = $potentialPageNode;
414  $pageNodeFoundByType = true;
415  break;
416  }
417  if (!$typeNumChild && $type === '0') {
418  // The first PAGE node that has no typeNum is considered '0' automatically.
419  $rawSetupPageNodeFromType = $potentialPageNode;
420  $pageNodeFoundByType = true;
421  break;
422  }
423  }
424  }
425  if (!$pageNodeFoundByType) {
426  $rawSetupPageNodeFromType = new RootNode();
427  }
428  $setupPageAst = new RootNode();
429  foreach ($rawSetupPageNodeFromType->getNextChild() as $child) {
430  $setupPageAst->addChild($child);
431  }
432 
433  if (!$gotSetupConfigFromCache) {
434  // If we did not get merged 'config.' from cache above, create it now and cache it.
435  $mergedSetupConfigAst = (new SetupConfigMerger())->merge($setupRawConfigAst, $setupPageAst->getChildByName('config'));
436  if ($mergedSetupConfigAst->getChildByName('absRefPrefix') === null) {
437  // Make sure config.absRefPrefix is set, fallback to 'auto'.
438  $absRefPrefixNode = new ChildNode('absRefPrefix');
439  $absRefPrefixNode->setValue('auto');
440  $mergedSetupConfigAst->addChild($absRefPrefixNode);
441  }
442  if ($mergedSetupConfigAst->getChildByName('doctype') === null) {
443  // Make sure config.doctype is set, fallback to 'html5'.
444  $doctypeNode = new ChildNode('doctype');
445  $doctypeNode->setValue('html5');
446  $mergedSetupConfigAst->addChild($doctypeNode);
447  }
448  if ($request) {
449  // Dispatch ModifyTypoScriptConfigEvent before config is cached and if Request is given.
450  $mergedSetupConfigAst = $this->eventDispatcher
451  ->dispatch(new ModifyTypoScriptConfigEvent($request, $setupAst, $mergedSetupConfigAst))->getConfigTree();
452  }
453  $frontendTypoScript->setConfigTree($mergedSetupConfigAst);
454  $setupConfigArray = $mergedSetupConfigAst->toArray();
455  $frontendTypoScript->setConfigArray($setupConfigArray);
456  $typoScriptCache?->set(
457  $setupConfigTypoScriptCacheIdentifier,
458  'return unserialize(\'' . addcslashes(serialize(['ast' => $mergedSetupConfigAst, 'array' => $setupConfigArray]), '\'\\') . '\');'
459  );
460  }
461 
462  if ($pageNodeFoundByType) {
463  // Remove "page.config" to prevent people from working with the not merged variant.
464  // We do *not* set page if it could not be determined (important for hasPage() later
465  // to return an early "no PAGE for type found" Response.
466  $setupPageAst->removeChildByName('config');
467  $frontendTypoScript->setPageTree($setupPageAst);
468  $frontendTypoScript->setPageArray($setupPageAst->toArray());
469  }
470  }
471  return $frontendTypoScript;
472  }
473 }
‪TYPO3\CMS\Core\Site\Entity\SiteInterface
Definition: SiteInterface.php:26
‪TYPO3\CMS\Core\TypoScript\AST\Merger\SetupConfigMerger
Definition: SetupConfigMerger.php:35
‪TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeSetupConditionConstantSubstitutionVisitor
Definition: IncludeTreeSetupConditionConstantSubstitutionVisitor.php:36
‪TYPO3\CMS\Core\Cache\Frontend\PhpFrontend
Definition: PhpFrontend.php:25
‪TYPO3\CMS\Frontend\Event\ModifyTypoScriptConfigEvent
Definition: ModifyTypoScriptConfigEvent.php:43
‪TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\IncludeTreeTraverser
Definition: IncludeTreeTraverser.php:30
‪TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeAstBuilderVisitor
Definition: IncludeTreeAstBuilderVisitor.php:39
‪TYPO3\CMS\Core\TypoScript
‪TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeConditionIncludeListAccumulatorVisitor
Definition: IncludeTreeConditionIncludeListAccumulatorVisitor.php:32
‪TYPO3\CMS\Frontend\Event\ModifyTypoScriptConstantsEvent
Definition: ModifyTypoScriptConstantsEvent.php:29
‪TYPO3\CMS\Core\TypoScript\AST\Node\ChildNode
Definition: ChildNode.php:23
‪TYPO3\CMS\Core\TypoScript\AST\Node\RootNode
Definition: RootNode.php:26
‪TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\ConditionVerdictAwareIncludeTreeTraverser
Definition: ConditionVerdictAwareIncludeTreeTraverser.php:38
‪TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\RootInclude
Definition: RootInclude.php:27
‪TYPO3\CMS\Core\TypoScript\Tokenizer\LossyTokenizer
Definition: LossyTokenizer.php:57
‪TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateTreeBuilder
Definition: SysTemplateTreeBuilder.php:74
‪TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeConditionMatcherVisitor
Definition: IncludeTreeConditionMatcherVisitor.php:44