*/ protected array $flatConstants = []; protected EventDispatcherInterface $eventDispatcher; protected function handleIdentifierUnsetLine(IdentifierUnsetLine $line, CurrentObjectPath $currentObjectPath): void { $node = $currentObjectPath->getFirst(); $identifierStream = $line->getIdentifierTokenStream()->reset(); while ($identifierToken = $identifierStream->getNext()) { if (!$foundNode = $node->getChildByName($identifierToken->getValue())) { break; } $nextIdentifierToken = $identifierStream->peekNext(); if ($nextIdentifierToken) { $node = $foundNode; continue; } $node->removeChildByName($identifierToken->getValue()); break; } } protected function handleIdentifierCopyLine(IdentifierCopyLine $line, RootNode $rootNode, CurrentObjectPath $currentObjectPath): ?NodeInterface { $sourceIdentifierStream = $line->getValueTokenStream()->reset(); $sourceNode = $rootNode; if ($sourceIdentifierStream->isRelative()) { // Entry node is current node from current object path if relative, otherwise RootNode. $sourceNode = $currentObjectPath->getLast(); } while ($identifierToken = $sourceIdentifierStream->getNext()) { // Go through source token stream and locate the sourceNode to copy from. if (!$sourceNode = $sourceNode->getChildByName($identifierToken->getValue())) { // Source node not found - nothing to do for this line return null; } } $isSourceNodeValueNull = true; if ($sourceNode->getValue() !== null) { // When the source node value is not null, it will override the target node value if that exists. $isSourceNodeValueNull = false; } // Locate/create the targets parent node the copied source should be added as child to, // and get the name of the node we're dealing with. $targetIdentifierTokenStream = $line->getIdentifierTokenStream()->reset(); $targetParentNode = $currentObjectPath->getFirst(); $targetTokenName = null; while ($targetToken = $targetIdentifierTokenStream->getNext()) { $targetTokenName = $targetToken->getValue(); if (!($targetIdentifierTokenStream->peekNext() ?? false)) { break; } if (!$foundNode = $targetParentNode->getChildByName($targetTokenName)) { // Add new node as new child of current last element in path $foundNode = new ChildNode($targetTokenName); $targetParentNode->addChild($foundNode); } $targetParentNode = $foundNode; } $existingTarget = null; if ($isSourceNodeValueNull) { // When the node to copy has no value, but the existing target has, // the value from the existing target is kept. Also, if the existing // node is a ReferenceChildNode and the source does not override this, // source children are added to the existing reference instead of // dropping the existing target. $existingTarget = $targetParentNode->getChildByName($targetTokenName); $existingTargetNodeValue = $existingTarget?->getValue(); } else { // Blindly remove existing target node if exists and the value is not overridden by source. $targetParentNode->removeChildByName($targetTokenName); } if ($existingTarget instanceof ReferenceChildNode) { // When existing target is a ReferenceChildNode, keep it and // copy children from source into existing target. $targetNode = $existingTarget; foreach ($sourceNode->getNextChild() as $sourceChild) { $targetNode->addChild(clone $sourceChild); } } else { // Clone full source node tree, update name and add as child to parent node. /** @var ChildNodeInterface $targetNode */ $targetNode = clone $sourceNode; $targetNode->updateName($targetTokenName); $targetParentNode->addChild($targetNode); } if ($isSourceNodeValueNull && $existingTargetNodeValue) { // If value of old existing target should be kept, set in now. $targetNode->setValue($existingTargetNodeValue); } return $targetNode; } /** * "foo =< bar": Prepare a reference resolving. * Note this does *not* resolve "=<" itself at this point since this operator can only be * evaluated after the full AST has been established. Also, having a full AST-traverser run * that does this is *very* expensive and "=<" is only done for "tt_content.myElement" and * "lib.parseFunc" anyways. As such, "=<" is NOT a language construct itself and the AST-parser * only marks nodes that use it by using the special node "ObjectReference". * Resolving then happens "lazy" and "on demand" in ContentObjectRenderer cObjGetSingle() * and mergeTSRef() for frontend "setup" TypoScript. */ protected function handleIdentifierReferenceLine(IdentifierReferenceLine $line, CurrentObjectPath $currentObjectPath): NodeInterface { $tokenStream = $line->getIdentifierTokenStream(); $node = $currentObjectPath->getFirst(); $identifierStream = $tokenStream->reset(); while ($identifierToken = $identifierStream->getNext()) { $nextIdentifier = $identifierStream->peekNext(); $identifierTokenValue = $identifierToken->getValue(); if (!($node->getChildByName($identifierTokenValue)) && $nextIdentifier) { // Add new node as new child of current last element in path $foundNode = new ChildNode($identifierTokenValue); $node->addChild($foundNode); } elseif (!$node->getChildByName($identifierTokenValue) && $nextIdentifier === null) { // Parent of target node exists, but target node does not. Add new reference child. $foundNode = new ReferenceChildNode($identifierTokenValue); $foundNode->setReferenceSourceStream($line->getValueTokenStream()); $node->addChild($foundNode); } elseif (($foundNode = $node->getChildByName($identifierTokenValue)) && $nextIdentifier === null) { // Target node exists already. We create a new one, remove old, but transfer existing children from old to new. $newNode = new ReferenceChildNode($identifierTokenValue); $newNode->setReferenceSourceStream($line->getValueTokenStream()); foreach ($foundNode->getNextChild() as $existingNodeChild) { $newNode->addChild($existingNodeChild); } $node->removeChildByName($identifierTokenValue); $node->addChild($newNode); $foundNode = $newNode; } $node = $foundNode; } return $node; } protected function getOrAddNodeFromIdentifierStream(CurrentObjectPath $currentObjectPath, IdentifierTokenStream $tokenStream): NodeInterface { $node = $currentObjectPath->getFirst(); $identifierStream = $tokenStream->reset(); while ($identifierToken = $identifierStream->getNext()) { $identifierTokenValue = $identifierToken->getValue(); if (!$foundNode = $node->getChildByName($identifierTokenValue)) { // Add new node as new child of current last element in path $foundNode = new ChildNode($identifierTokenValue); $node->addChild($foundNode); } $node = $foundNode; } return $node; } /** * Evaluate operator functions, example TypoScript: * "page.10.value := appendString(foo)" */ protected function evaluateValueModifier(Token $functionNameToken, TokenStreamInterface $functionArgumentTokenStream, ?string $originalValue): ?string { $functionName = $functionNameToken->getValue(); // Constants are evaluated via __toString() of ConstantAwareTokenStream and thus need current constants. // This implements constants in function arguments: "foo := addToList({$my.constant})" if ($functionArgumentTokenStream instanceof ConstantAwareTokenStream) { $functionArgumentTokenStream->setFlatConstants($this->flatConstants); } $functionArgument = (string)$functionArgumentTokenStream; switch ($functionName) { case 'prependString': return $functionArgument . $originalValue; case 'appendString': return $originalValue . $functionArgument; case 'removeString': return str_replace($functionArgument, '', $originalValue); case 'replaceString': $functionValueArray = explode('|', $functionArgument, 2); $fromStr = $functionValueArray[0]; $toStr = $functionValueArray[1] ?? ''; return str_replace($fromStr, $toStr, $originalValue); case 'addToList': return ($originalValue !== null ? $originalValue . ',' : '') . $functionArgument; case 'removeFromList': $existingElements = GeneralUtility::trimExplode(',', $originalValue ?? ''); $removeElements = GeneralUtility::trimExplode(',', $functionArgument); if (!empty($removeElements)) { return implode(',', array_diff($existingElements, $removeElements)); } return $originalValue; case 'uniqueList': $elements = GeneralUtility::trimExplode(',', $originalValue ?? ''); return implode(',', array_unique($elements)); case 'reverseList': $elements = GeneralUtility::trimExplode(',', $originalValue ?? ''); return implode(',', array_reverse($elements)); case 'sortList': $elements = GeneralUtility::trimExplode(',', $originalValue ?? ''); $arguments = GeneralUtility::trimExplode(',', $functionArgument); $arguments = array_map('strtolower', $arguments); $sortFlags = SORT_REGULAR; if (in_array('numeric', $arguments)) { $sortFlags = SORT_NUMERIC; // If the sorting modifier "numeric" is given, all values // are checked and an exception is thrown if a non-numeric value is given // otherwise there is a different behaviour between PHP 7 and PHP 5.x // See also the warning on http://us.php.net/manual/en/function.sort.php foreach ($elements as $element) { if (!is_numeric($element)) { throw new \InvalidArgumentException( 'The list "' . $originalValue . '" should be sorted numerically but contains a non-numeric value', 1650893781 ); } } } sort($elements, $sortFlags); if (in_array('descending', $arguments)) { $elements = array_reverse($elements); } return implode(',', $elements); case 'getEnv': $environmentValue = getenv(trim($functionArgument)); if ($environmentValue !== false) { return $environmentValue; } return $originalValue; default: return $this->eventDispatcher->dispatch(new EvaluateModifierFunctionEvent($functionName, $functionArgument, $originalValue))->getValue() ?? $originalValue; } } }