/** * @test */ public function overridingFromTypoScriptInFilesystemFollowingNodePathsWorks() { $typoScriptService = $this->objectManager->get('TYPO3\\TYPO3\\Domain\\Service\\TypoScriptService'); ObjectAccess::setProperty($typoScriptService, 'typoScriptsPathPattern', __DIR__ . '/Fixtures/ResourcesFixture/TypoScripts', TRUE); $objectTree = $typoScriptService->getMergedTypoScriptObjectTree($this->homeNode, $this->homeNode->getNode('about-us/history')); $this->assertEquals('Root', $objectTree['text1']['value']); $this->assertEquals('AboutUs', $objectTree['text2']['value']); $this->assertEquals('History', $objectTree['text3']['value']); $this->assertEquals('Additional', $objectTree['text4']['value']); }
/** * Creates a new comment * * @param \TYPO3\TYPO3CR\Domain\Model\NodeInterface $postNode The post node which will contain the new comment * @param \TYPO3\TYPO3CR\Domain\Model\NodeTemplate<RobertLemke.Plugin.Blog:Comment> $nodeTemplate * @return void */ public function createAction(NodeInterface $postNode, NodeTemplate $newComment) { # Workaround until we can validate node templates properly: if (strlen($newComment->getProperty('author')) < 2) { $this->addFlashMessage('Your comment was NOT created - please specify your name.'); $this->redirect('show', 'Frontend\\Node', 'TYPO3.Neos', array('node' => $postNode)); } if (strlen($newComment->getProperty('text')) < 5) { $this->addFlashMessage('Your comment was NOT created - it was too short.'); $this->redirect('show', 'Frontend\\Node', 'TYPO3.Neos', array('node' => $postNode)); } if (filter_var($newComment->getProperty('emailAddress'), FILTER_VALIDATE_EMAIL) === FALSE) { $this->addFlashMessage('Your comment was NOT created - you must specify a valid email address.'); $this->redirect('show', 'Frontend\\Node', 'TYPO3.Neos', array('node' => $postNode)); } $commentNode = $postNode->getNode('comments')->createNodeFromTemplate($newComment, uniqid('comment-')); $commentNode->setProperty('spam', FALSE); $commentNode->setProperty('datePublished', new \DateTime()); if ($this->akismetService->isCommentSpam('', $commentNode->getProperty('text'), 'comment', $commentNode->getProperty('author'), $commentNode->getProperty('emailAddress'))) { $commentNode->setProperty('spam', TRUE); } $this->addFlashMessage('Your new comment was created.'); $this->emitCommentCreated($commentNode, $postNode); $this->redirect('show', 'Frontend\\Node', 'TYPO3.Neos', array('node' => $postNode)); }
/** * @param array $hits * @return array Array of Node objects */ protected function convertHitsToNodes(array $hits) { $nodes = []; $elasticSearchHitPerNode = []; /** * TODO: This code below is not fully correct yet: * * We always fetch $limit * (numerOfWorkspaces) records; so that we find a node: * - *once* if it is only in live workspace and matches the query * - *once* if it is only in user workspace and matches the query * - *twice* if it is in both workspaces and matches the query *both times*. In this case we filter the duplicate record. * - *once* if it is in the live workspace and has been DELETED in the user workspace (STILL WRONG) * - *once* if it is in the live workspace and has been MODIFIED to NOT MATCH THE QUERY ANYMORE in user workspace (STILL WRONG) * * If we want to fix this cleanly, we'd need to do an *additional query* in order to filter all nodes from a non-user workspace * which *do exist in the user workspace but do NOT match the current query*. This has to be done somehow "recursively"; and later * we might be able to use https://github.com/elasticsearch/elasticsearch/issues/3300 as soon as it is merged. */ foreach ($hits['hits'] as $hit) { $nodePath = current($hit['fields']['__path']); $node = $this->contextNode->getNode($nodePath); if ($node instanceof NodeInterface && !isset($nodes[$node->getIdentifier()])) { $nodes[$node->getIdentifier()] = $node; $elasticSearchHitPerNode[$node->getIdentifier()] = $hit; if ($this->limit > 0 && count($nodes) >= $this->limit) { break; } } } if ($this->logThisQuery === true) { $this->logger->log('Returned nodes (' . $this->logMessage . '): ' . count($nodes), LOG_DEBUG); } $this->elasticSearchHitsIndexedByNodeFromLastRequest = $elasticSearchHitPerNode; return array_values($nodes); }
/** * @test */ public function linkingServiceStoresLastLinkedNode() { $targetNodeA = $this->baseNode; $targetNodeB = $this->baseNode->getNode('about-us'); $this->linkingService->createNodeUri($this->controllerContext, $targetNodeA); $this->assertSame($targetNodeA, $this->linkingService->getLastLinkedNode()); $this->linkingService->createNodeUri($this->controllerContext, $targetNodeB); $this->assertSame($targetNodeB, $this->linkingService->getLastLinkedNode()); }
/** * @test */ public function moveAfterInPersonalWorkspaceDoesNotAffectLiveWorkspace() { // move "teaser" after "main/dummy42" $teaserTestWorkspace = $this->nodeInTestWorkspace->getNode('teaser'); $teaserTestWorkspace->moveAfter($this->nodeInTestWorkspace->getNode('main/dummy42')); $this->assertNull($this->nodeInTestWorkspace->getNode('teaser/dummy42a'), 'moving not successful (1)'); $this->assertNotNull($this->nodeInTestWorkspace->getNode('main/teaser/dummy42a'), 'moving not successful (2)'); $this->assertNotNull($this->node->getNode('teaser'), 'moving shined through into live workspace (1)'); $this->assertNotNull($this->node->getNode('teaser/dummy42a'), 'moving shined through into live workspace (2)'); $this->assertNull($this->node->getNode('main/teaser/dummy42a'), 'moving shined through into live workspace (3)'); }
/** * Execute the query and return the list of nodes as result. * * This method is rather internal; just to be called from the ElasticSearchQueryResult. For the public API, please use execute() * * @return array<\TYPO3\TYPO3CR\Domain\Model\NodeInterface> */ public function fetch() { $timeBefore = microtime(TRUE); $response = $this->elasticSearchClient->getIndex()->request('GET', '/_search', array(), json_encode($this->request)); $timeAfterwards = microtime(TRUE); $treatedContent = $response->getTreatedContent(); $hits = $treatedContent['hits']; if ($this->logThisQuery === TRUE) { $this->logger->log('Query Log (' . $this->logMessage . '): ' . json_encode($this->request) . ' -- execution time: ' . ($timeAfterwards - $timeBefore) * 1000 . ' ms -- Limit: ' . $this->limit . ' -- Number of results returned: ' . count($hits['hits']) . ' -- Total Results: ' . $hits['total'], LOG_DEBUG); } $this->totalItems = $hits['total']; if ($hits['total'] === 0) { return array(); } $nodes = array(); $elasticSearchHitPerNode = array(); /** * TODO: This code below is not fully correct yet: * * We always fetch $limit * (numerOfWorkspaces) records; so that we find a node: * - *once* if it is only in live workspace and matches the query * - *once* if it is only in user workspace and matches the query * - *twice* if it is in both workspaces and matches the query *both times*. In this case we filter the duplicate record. * - *once* if it is in the live workspace and has been DELETED in the user workspace (STILL WRONG) * - *once* if it is in the live workspace and has been MODIFIED to NOT MATCH THE QUERY ANYMORE in user workspace (STILL WRONG) * * If we want to fix this cleanly, we'd need to do an *additional query* in order to filter all nodes from a non-user workspace * which *do exist in the user workspace but do NOT match the current query*. This has to be done somehow "recursively"; and later * we might be able to use https://github.com/elasticsearch/elasticsearch/issues/3300 as soon as it is merged. */ foreach ($hits['hits'] as $hit) { // with ElasticSearch 1.0 fields always returns an array, // see https://github.com/Flowpack/Flowpack.ElasticSearch.ContentRepositoryAdaptor/issues/17 if (is_array($hit['fields']['__path'])) { $nodePath = current($hit['fields']['__path']); } else { $nodePath = $hit['fields']['__path']; } $node = $this->contextNode->getNode($nodePath); if ($node instanceof NodeInterface) { $nodes[$node->getIdentifier()] = $node; $elasticSearchHitPerNode[$node->getIdentifier()] = $hit; if ($this->limit > 0 && count($nodes) >= $this->limit) { break; } } } if ($this->logThisQuery === TRUE) { $this->logger->log('Query Log (' . $this->logMessage . ') Number of returned results: ' . count($nodes), LOG_DEBUG); } $this->elasticSearchHitsIndexedByNodeFromLastRequest = $elasticSearchHitPerNode; $this->elasticSearchAggregationsFromLastRequest = $treatedContent['aggregations']; return array_values($nodes); }
/** * Set up the following node structure: * * /headline (TYPO3.TYPO3CR.Testing:Headline) * - live workspace * - title: Hello World * - personal workspace * - title: Hello World * - subtitle: Brave new world * /headline with language=de_DE * - personal workspace * - title: Hallo Welt * @return void */ protected function setupNodeWithShadowNodeInPersonalWorkspace() { $nodeTypeManager = $this->objectManager->get(NodeTypeManager::class); $headlineNode = $this->rootNodeInLiveWorkspace->createNode('headline', $nodeTypeManager->getNodeType('TYPO3.TYPO3CR.Testing:Headline')); $headlineNode->setProperty('title', 'Hello World'); $headlineNodeInPersonalWorkspace = $this->rootNodeInPersonalWorkspace->getNode('headline'); $headlineNodeInPersonalWorkspace->setProperty('subtitle', 'Brave new world'); $germanContext = $this->contextFactory->create(array('workspaceName' => $this->currentTestWorkspaceName, 'dimensions' => array('language' => array('de_DE', 'mul_ZZ')))); $headlineInGerman = $germanContext->getNode('/headline'); $headlineInGerman->setProperty('title', 'Hallo Welt'); $this->flushNodeChanges(); }
/** * Check if the given node is already a collection, find collection by nodePath otherwise, throw exception * if no content collection could be found * * @param NodeInterface $node * @param string $nodePath * @return NodeInterface * @throws Exception */ public function nearestContentCollection(NodeInterface $node, $nodePath) { $contentCollectionType = 'TYPO3.Neos:ContentCollection'; if ($node->getNodeType()->isOfType($contentCollectionType)) { return $node; } else { if ((string) $nodePath === '') { throw new Exception(sprintf('No content collection of type %s could be found in the current node and no node path was provided. You might want to configure the nodePath property with a relative path to the content collection.', $contentCollectionType), 1409300545); } $subNode = $node->getNode($nodePath); if ($subNode !== null && $subNode->getNodeType()->isOfType($contentCollectionType)) { return $subNode; } else { throw new Exception(sprintf('No content collection of type %s could be found in the current node (%s) or at the path "%s". You might want to adjust your node type configuration and create the missing child node through the "flow node:repair --node-type %s" command.', $contentCollectionType, $node->getPath(), $nodePath, (string) $node->getNodeType()), 1389352984); } } }
/** * Execute the query and return the list of nodes as result * * @return array<\TYPO3\TYPO3CR\Domain\Model\NodeInterface> */ public function execute() { // Adding implicit sorting by __sortIndex (as last fallback) as we can expect it to be there for nodes. $this->sorting[] = 'objects.__sortIndex ASC'; $timeBefore = microtime(TRUE); $result = parent::execute(); $timeAfterwards = microtime(TRUE); if ($this->queryLogEnabled === TRUE) { $this->logger->log('Query Log (' . $this->logMessage . '): -- execution time: ' . ($timeAfterwards - $timeBefore) * 1000 . ' ms -- Total Results: ' . count($result), LOG_DEBUG); } if (empty($result)) { return array(); } $nodes = array(); foreach ($result as $hit) { $nodePath = $hit['__path']; $node = $this->contextNode->getNode($nodePath); if ($node instanceof NodeInterface) { $nodes[$node->getIdentifier()] = $node; } } return array_values($nodes); }
/** * Renders the given Node as a teaser text with up to 600 characters, with all <p> and <a> tags removed. * * @param NodeInterface $node * @return mixed */ public function renderTeaser(NodeInterface $node) { $stringToTruncate = ''; foreach ($node->getNode('main')->getChildNodes('TYPO3.Neos.NodeTypes:Text') as $contentNode) { foreach ($contentNode->getProperties() as $propertyValue) { if (!is_object($propertyValue) || method_exists($propertyValue, '__toString')) { $stringToTruncate .= $propertyValue; } } } $jumpPosition = strpos($stringToTruncate, '<!-- read more -->'); if ($jumpPosition !== FALSE) { return $this->stripUnwantedTags(substr($stringToTruncate, 0, $jumpPosition - 1)); } $jumpPosition = strpos($stringToTruncate, '</p>'); if ($jumpPosition !== FALSE && $jumpPosition < 600) { return $this->stripUnwantedTags(substr($stringToTruncate, 0, $jumpPosition + 4)); } if (strlen($stringToTruncate) > 500) { return substr($this->stripUnwantedTags($stringToTruncate), 0, 501) . ' ...'; } else { return $this->stripUnwantedTags($stringToTruncate); } }
/** * Copies this node into the given node * * @param \TYPO3\TYPO3CR\Domain\Model\NodeInterface $referenceNode * @param string $nodeName * @return \TYPO3\TYPO3CR\Domain\Model\NodeInterface * @throws NodeExistsException * @api */ public function copyInto(NodeInterface $referenceNode, $nodeName) { if ($referenceNode->getNode($nodeName) !== NULL) { throw new NodeExistsException('Node with path "' . $referenceNode->getPath() . '/' . $nodeName . '" already exists.', 1292503467); } if (!$this->isNodeDataMatchingContext()) { $this->materializeNodeData(); } $copiedNode = $this->createRecursiveCopy($referenceNode, $nodeName); $this->context->getFirstLevelNodeCache()->flush(); $this->emitNodeAdded($copiedNode); return $copiedNode; }
/** * Iterates over the nodes and adds them to the workspace. * * @param \SimpleXMLElement $parentXml * @param \TYPO3\TYPO3CR\Domain\Model\NodeInterface $parentNode * @return void */ protected function parseNodes(\SimpleXMLElement $parentXml, \TYPO3\TYPO3CR\Domain\Model\NodeInterface $parentNode) { foreach ($parentXml->node as $childNodeXml) { $childNode = $parentNode->getNode((string) $childNodeXml['nodeName']); $contentTypeName = (string) $childNodeXml['type']; if (!$this->contentTypeManager->hasContentType($contentTypeName)) { $contentType = $this->contentTypeManager->createContentType($contentTypeName); } else { $contentType = $this->contentTypeManager->getContentType($contentTypeName); } if ($childNode === NULL) { $identifier = (string) $childNodeXml['identifier'] === '' ? NULL : (string) $childNodeXml['identifier']; $childNode = $parentNode->createSingleNode((string) $childNodeXml['nodeName'], $contentType, $identifier); } else { $childNode->setContentType($contentType); } $childNode->setHidden((bool) $childNodeXml['hidden']); $childNode->setHiddenInIndex((bool) $childNodeXml['hiddenInIndex']); if ($childNodeXml['hiddenBeforeDateTime'] != '') { $childNode->setHiddenBeforeDateTime(\DateTime::createFromFormat(\DateTime::W3C, (string) $childNodeXml['hiddenBeforeDateTime'])); } if ($childNodeXml['hiddenAfterDateTime'] != '') { $childNode->setHiddenAfterDateTime(\DateTime::createFromFormat(\DateTime::W3C, (string) $childNodeXml['hiddenAfterDateTime'])); } if ($childNodeXml->properties) { foreach ($childNodeXml->properties->children() as $childXml) { if (isset($childXml['__type']) && (string) $childXml['__type'] == 'object') { $childNode->setProperty($childXml->getName(), $this->xmlToObject($childXml)); } else { $childNode->setProperty($childXml->getName(), (string) $childXml); } } } if ($childNodeXml->accessRoles) { $accessRoles = array(); foreach ($childNodeXml->accessRoles->children() as $childXml) { $accessRoles[] = (string) $childXml; } $childNode->setAccessRoles($accessRoles); } if ($childNodeXml->node) { $this->parseNodes($childNodeXml, $childNode); } } }
/** * Converts the given $nodeXml to a node and adds it to the $parentNode (or overrides an existing node) * * @param \SimpleXMLElement $nodeXml * @param NodeInterface $parentNode * @return void */ protected function importNode(\SimpleXMLElement $nodeXml, NodeInterface $parentNode) { $nodeName = (string) $nodeXml['nodeName']; $nodeType = $this->parseNodeType($nodeXml); $node = $parentNode->getNode($nodeName); if ($node === null) { $identifier = (string) $nodeXml['identifier'] === '' ? null : (string) $nodeXml['identifier']; $node = $parentNode->createSingleNode((string) $nodeXml['nodeName'], $nodeType, $identifier); } else { $node->setNodeType($nodeType); } $this->importNodeVisibility($nodeXml, $node); $this->importNodeProperties($nodeXml, $node); $this->importNodeAccessRoles($nodeXml, $node); if ($nodeXml->node) { foreach ($nodeXml->node as $childNodeXml) { $this->importNode($childNodeXml, $node); } } }
/** * Internal method to do the actual copying. * * For behavior of the $detachedCopy parameter, see method Node::createRecursiveCopy(). * * @param NodeInterface $referenceNode * @param $nodeName * @param boolean $detachedCopy * @return NodeInterface * @throws NodeConstraintException * @throws NodeExistsException */ protected function copyIntoInternal(NodeInterface $referenceNode, $nodeName, $detachedCopy) { if ($referenceNode->getNode($nodeName) !== null) { throw new NodeExistsException('Node with path "' . $referenceNode->getPath() . '/' . $nodeName . '" already exists.', 1292503467); } // On copy we basically re-recreate an existing node on a new location. As we skip the constraints check on // node creation we should do the same while writing the node on the new location. if (!$referenceNode->willChildNodeBeAutoCreated($nodeName) && !$referenceNode->isNodeTypeAllowedAsChildNode($this->getNodeType())) { throw new NodeConstraintException(sprintf('Cannot copy "%s" into "%s" due to node type constraints.', $this->__toString(), $referenceNode->__toString()), 1404648177); } $copiedNode = $this->createRecursiveCopy($referenceNode, $nodeName, $detachedCopy); $this->context->getFirstLevelNodeCache()->flush(); $this->emitNodeAdded($copiedNode); return $copiedNode; }
/** * @param NodeInterface $documentNode */ protected function createBatchContentNodes(NodeInterface $documentNode) { $mainContentCollection = $documentNode->getNode('main'); for ($j = 0; $j < $this->preset->getContentNodeByDocument(); $j++) { try { $nodeType = $this->nodeTypeManager->getNodeType($this->preset->getContentNodeType()); $generator = $this->getNodeGeneratorImplementationClassByNodeType($nodeType); $generator->create($mainContentCollection, $nodeType); } catch (NodeExistsException $e) { } } }
/** * Imports the specified bundle into the configured "importRootNodePath". * * @param string $bundle * @return void */ protected function importBundle($bundle) { $nodeTypes = array('page' => $this->nodeTypeManager->getNodeType($this->bundleConfiguration['nodeTypes']['page']), 'section' => $this->nodeTypeManager->getNodeType($this->bundleConfiguration['nodeTypes']['section']), 'text' => $this->nodeTypeManager->getNodeType($this->bundleConfiguration['nodeTypes']['text'])); $this->outputLine('Importing bundle "%s"', array($bundle)); $renderedDocumentationRootPath = rtrim($this->bundleConfiguration['renderedDocumentationRootPath'], '/'); $importRootNode = $this->siteNode->getNode($this->bundleConfiguration['importRootNodePath']); if ($importRootNode === NULL) { $this->output('ImportRootNode "%s" does not exist!', array($this->bundleConfiguration['importRootNodePath'])); $this->quit(1); } if (!is_dir($renderedDocumentationRootPath)) { $this->outputLine('The folder "%s" does not exist. Did you render the documentation?', array($renderedDocumentationRootPath)); $this->quit(1); } $unorderedJsonFileNames = Files::readDirectoryRecursively($renderedDocumentationRootPath, '.fjson'); if ($unorderedJsonFileNames === array()) { $this->outputLine('The folder "%s" contains no fjson files. Did you render the documentation?', array($renderedDocumentationRootPath)); $this->quit(1); } $orderedNodePaths = array(); foreach ($unorderedJsonFileNames as $jsonPathAndFileName) { if (basename($jsonPathAndFileName) === 'Index.fjson') { $chapterRelativeNodePath = substr($jsonPathAndFileName, strlen($renderedDocumentationRootPath), -12) . '/'; $indexArray = json_decode(file_get_contents($jsonPathAndFileName), TRUE); foreach (explode(chr(10), $indexArray['body']) as $tocHtmlLine) { preg_match('!^\\<li class="toctree-l1"\\>\\<a class="reference internal" href="\\.\\./([a-zA-Z0-9-]+)/.*$!', $tocHtmlLine, $matches); if ($matches !== array()) { $orderedNodePaths[] = $this->normalizeNodePath($chapterRelativeNodePath . $matches[1]); } } } } foreach ($unorderedJsonFileNames as $jsonPathAndFileName) { $data = json_decode(file_get_contents($jsonPathAndFileName)); if (!isset($data->body)) { continue; } $relativeNodePath = substr($jsonPathAndFileName, strlen($renderedDocumentationRootPath) + 1, -6); $relativeNodePath = $this->normalizeNodePath($relativeNodePath); $segments = explode('/', $relativeNodePath); $pageNode = $importRootNode; while ($segment = array_shift($segments)) { $nodeName = preg_replace('/[^a-z0-9\\-]/', '', $segment); $subPageNode = $pageNode->getNode($nodeName); if ($subPageNode === NULL) { $this->outputLine('Creating page node "%s"', array($relativeNodePath)); /** @var NodeInterface $subPageNode */ $subPageNode = $pageNode->createNode($nodeName, $nodeTypes['page']); if (!$subPageNode->hasProperty('title')) { $subPageNode->setProperty('title', $nodeName); } } else { $subPageNode->setNodeType($nodeTypes['page']); } $pageNode = $subPageNode; } $sectionNode = $pageNode->getNode('main'); if ($sectionNode === NULL) { $this->outputLine('Creating section node "%s"', array($relativeNodePath . '/main')); $sectionNode = $pageNode->createNode('main', $nodeTypes['section']); } else { $sectionNode->setNodeType($nodeTypes['section']); } $textNode = $sectionNode->getNode('text1'); if ($textNode === NULL) { $this->outputLine('Creating text node "%s"', array($relativeNodePath . '/main/text1')); $textNode = $sectionNode->createNode('text1', $nodeTypes['text']); } else { $textNode->setNodeType($nodeTypes['text']); } $pageNode->setProperty('title', htmlspecialchars_decode($data->title)); $this->outputLine('Setting page title of page "%s" to "%s"', array($relativeNodePath, $data->title)); $bodyText = $this->prepareBodyText($data->body, $relativeNodePath); $textNode->setProperty('title', ''); $textNode->setProperty('text', $bodyText); } $importRootNodePath = $importRootNode->getPath(); $currentParentNodePath = ''; /** @var NodeInterface $previousNode */ $previousNode = NULL; foreach ($orderedNodePaths as $nodePath) { $node = $importRootNode->getNode($importRootNodePath . $nodePath); if ($node !== NULL) { if ($node->getParent()->getPath() !== $currentParentNodePath) { $currentParentNodePath = $node->getParent()->getPath(); $previousNode = NULL; } if ($previousNode !== NULL) { $this->outputLine('Moved node %s', array($node->getPath())); $this->outputLine('after node %s', array($previousNode->getPath())); $node->moveAfter($previousNode); } else { // FIXME: Node->isFirst() or Node->moveFirst() would be needed here } $previousNode = $node; } else { $this->outputLine('Node %s does not exist.', array($importRootNodePath . $nodePath)); } } $this->siteRepository->update($this->currentSite); }
/** * @param string $name * @param string $externalIdentifier * @param string $nodeName * @param NodeInterface $storageNode * @param boolean $skipExistingNode * @return boolean */ protected function skipNodeProcessing($name, $externalIdentifier, $nodeName, NodeInterface $storageNode, $skipExistingNode = TRUE, $skipAlreadyProcessed = TRUE) { if ($skipAlreadyProcessed === TRUE && $this->getNodeProcessing($externalIdentifier)) { $this->log(sprintf('- Skip already processed node "%s" ...', $name), LOG_NOTICE); return TRUE; } $node = $storageNode->getNode($nodeName); if ($skipExistingNode === TRUE && $node instanceof NodeInterface) { $this->log(sprintf('- Skip existing node "%s" ...', $name), LOG_WARNING); $this->registerNodeProcessing($node, $externalIdentifier); return TRUE; } return FALSE; }
/** * @param string $idealNodeName * @param NodeInterface $referenceNode * @return string */ protected function getFreeNodeName($idealNodeName, NodeInterface $referenceNode) { $idealNodeName = NodeUtility::renderValidNodeName($idealNodeName); $possibleNodeName = $idealNodeName; $counter = 1; while ($referenceNode->getNode($possibleNodeName) !== null) { $possibleNodeName = $idealNodeName . '-' . $counter; $counter++; } return $possibleNodeName; }