From 1d986c9496ded048dc913ad064329be2c3a76ca9 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Fri, 3 Nov 2023 14:16:57 +0100 Subject: [PATCH 01/11] TASK: Adjust test for the new cr --- .../Features/Fusion/FlowQuery.feature | 97 ++++++++++++------- 1 file changed, 64 insertions(+), 33 deletions(-) diff --git a/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature b/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature index ee4ec0f3ada..aa6447e947a 100644 --- a/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature +++ b/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature @@ -1,25 +1,29 @@ -@fixtures +@flowEntities @contentrepository Feature: Tests for the "Neos.ContentRepository" Flow Query methods. Background: - Given I have the site "a" - And I have the following NodeTypes configuration: + Given using no content dimensions + And using the following node types: """yaml - 'unstructured': {} - 'Neos.Neos:FallbackNode': {} + 'Neos.ContentRepository:Root': {} + 'Neos.Neos:Sites': + superTypes: + 'Neos.ContentRepository:Root': true 'Neos.Neos:Document': properties: title: type: string uriPathSegment: type: string + _hiddenInIndex: + type: bool + 'Neos.Neos:Site': + superTypes: + 'Neos.Neos:Document': true 'Neos.Neos:Content': properties: title: type: string - 'Neos.Neos:Test.Site': - superTypes: - 'Neos.Neos:Document': true 'Neos.Neos:Test.DocumentType1': superTypes: 'Neos.Neos:Document': true @@ -32,28 +36,55 @@ Feature: Tests for the "Neos.ContentRepository" Flow Query methods. 'Neos.Neos:Test.Content': superTypes: 'Neos.Neos:Content': true + + """ + And using identifier "default", I define a content repository + And I am in content repository "default" + And I am user identified by "initiating-user-identifier" + + When the command CreateRootWorkspace is executed with payload: + | Key | Value | + | workspaceName | "live" | + | newContentStreamId | "cs-identifier" | + And the command CreateRootNodeAggregateWithNode is executed with payload: + | Key | Value | + | contentStreamId | "cs-identifier" | + | nodeAggregateId | "root" | + | nodeTypeName | "Neos.Neos:Sites" | + And the graph projection is fully up to date + And I am in content stream "cs-identifier" and dimension space point {} + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | parentNodeAggregateId | nodeTypeName | initialPropertyValues | nodeName | + | a | root | Neos.Neos:Site | {"title": "Node a"} | a | + | a1 | a | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1", "title": "Node a1"} | a1 | + | a1a | a1 | Neos.Neos:Test.DocumentType2a | {"uriPathSegment": "a1a", "title": "Node a1a"} | a1a | + | a1a1 | a1a | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1a1", "title": "Node a1a1"} | a1a1 | + | a1a2 | a1a | Neos.Neos:Test.DocumentType2 | {"uriPathSegment": "a1a2", "title": "Node a1a2"} | a1a2 | + | a1a3 | a1a | Neos.Neos:Test.DocumentType2a | {"uriPathSegment": "a1a3", "title": "Node a1a3"} | a1a3 | + | a1a4 | a1a | Neos.Neos:Test.DocumentType2a | {"uriPathSegment": "a1a4", "title": "Node a1a4"} | a1a4 | + | a1a5 | a1a | Neos.Neos:Test.DocumentType2a | {"uriPathSegment": "a1a5", "title": "Node a1a5"} | a1a5 | + | a1a6 | a1a | Neos.Neos:Test.DocumentType2 | {"uriPathSegment": "a1a6", "title": "Node a1a6"} | a1a6 | + | a1a7 | a1a | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1a7", "title": "Node a1a7"} | a1a7 | + | a1b | a1 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1b", "title": "Node a1b"} | a1b | + | a1b1 | a1b | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1b1", "title": "Node a1b1"} | a1b1 | + | a1b1a | a1b1 | Neos.Neos:Test.DocumentType2a | {"uriPathSegment": "a1b1a", "title": "Node a1b1a"} | a1b1a | + | a1b1b | a1b1 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1b1b", "title": "Node a1b1b"} | a1b1b | + | a1b2 | a1b | Neos.Neos:Test.DocumentType2 | {"uriPathSegment": "a1b2", "title": "Node a1b2"} | a1b2 | + | a1b3 | a1b | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1b3", "title": "Node a1b3"} | a1b3 | + | a1c | a1 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1c", "title": "Node a1c", "_hiddenInIndex": true} | a1c | + | a1c1 | a1c | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1c1", "title": "Node a1c1"} | a1c1 | + And A site exists for node name "a" and domain "http://localhost" + And the sites configuration is: + """yaml + Neos: + Neos: + sites: + '*': + contentRepository: default + contentDimensions: + resolver: + factoryClassName: Neos\Neos\FrontendRouting\DimensionResolution\Resolver\NoopResolverFactory """ - And I have the following nodes: - | Identifier | Path | Node Type | Properties | Hidden in index | - | root | /sites | unstructured | | false | - | a | /sites/a | Neos.Neos:Test.Site | {"uriPathSegment": "a", "title": "Node a"} | false | - | a1 | /sites/a/a1 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1", "title": "Node a1"} | false | - | a1a | /sites/a/a1/a1a | Neos.Neos:Test.DocumentType2a | {"uriPathSegment": "a1a", "title": "Node a1a"} | false | - | a1a1 | /sites/a/a1/a1a/a1a1 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1a1", "title": "Node a1a1"} | false | - | a1a2 | /sites/a/a1/a1a/a1a2 | Neos.Neos:Test.DocumentType2 | {"uriPathSegment": "a1a2", "title": "Node a1a2"} | false | - | a1a3 | /sites/a/a1/a1a/a1a3 | Neos.Neos:Test.DocumentType2a | {"uriPathSegment": "a1a3", "title": "Node a1a3"} | false | - | a1a4 | /sites/a/a1/a1a/a1a4 | Neos.Neos:Test.DocumentType2a | {"uriPathSegment": "a1a4", "title": "Node a1a4"} | false | - | a1a5 | /sites/a/a1/a1a/a1a5 | Neos.Neos:Test.DocumentType2a | {"uriPathSegment": "a1a5", "title": "Node a1a5"} | false | - | a1a6 | /sites/a/a1/a1a/a1a6 | Neos.Neos:Test.DocumentType2 | {"uriPathSegment": "a1a6", "title": "Node a1a6"} | false | - | a1a7 | /sites/a/a1/a1a/a1a7 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1a7", "title": "Node a1a7"} | false | - | a1b | /sites/a/a1/a1b | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1b", "title": "Node a1b"} | false | - | a1b1 | /sites/a/a1/a1b/a1b1 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1b1", "title": "Node a1b1"} | false | - | a1b1a | /sites/a/a1/a1b/a1b1/a1b1a | Neos.Neos:Test.DocumentType2a | {"uriPathSegment": "a1b1a", "title": "Node a1b1a"} | false | - | a1b1b | /sites/a/a1/a1b/a1b1/a1b1b | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1b1b", "title": "Node a1b1b"} | false | - | a1b2 | /sites/a/a1/a1b/a1b2 | Neos.Neos:Test.DocumentType2 | {"uriPathSegment": "a1b2", "title": "Node a1b2"} | false | - | a1b3 | /sites/a/a1/a1b/a1b3 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1b3", "title": "Node a1b3"} | false | - | a1c | /sites/a/a1/a1c | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1c", "title": "Node a1c"} | true | - | a1c1 | /sites/a/a1/a1c/a1c1 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1c1", "title": "Node a1c1"} | false | And the Fusion context node is "a1a4" And the Fusion context request URI is "http://localhost" And I have the following Fusion setup: @@ -66,7 +97,7 @@ Feature: Tests for the "Neos.ContentRepository" Flow Query methods. renderer = Neos.Fusion:Loop { items = ${props.nodes} itemName = 'node' - itemRenderer = ${node.identifier} + itemRenderer = ${node.nodeAggregateId.value} @glue = ',' } } @@ -164,7 +195,7 @@ Feature: Tests for the "Neos.ContentRepository" Flow Query methods. When I execute the following Fusion code: """fusion test = Neos.Fusion:DataStructure { - criteria = ${q(node).parentsUntil('[instanceof Neos.Neos:Test.Site]').get()} + criteria = ${q(node).parentsUntil('[instanceof Neos.Neos:Site]').get()} # this does not work in Neos 8.3 but it should according to documentation and yield "a1a" # criteriaAndFilter = ${q(node).parentsUntil('[instanceof Neos.Neos:Test.DocumentType1]', '[instanceof Neos.Neos:Test.DocumentType2]').get()} @process.render = Neos.Neos:Test.RenderNodesDataStructure @@ -180,7 +211,7 @@ Feature: Tests for the "Neos.ContentRepository" Flow Query methods. """fusion test = Neos.Fusion:DataStructure { upToType = ${q(node).closest('[instanceof Neos.Neos:Test.DocumentType1]').get()} - upToSite = ${q(node).closest('[instanceof Neos.Neos:Test.Site]').get()} + upToSite = ${q(node).closest('[instanceof Neos.Neos:Site]').get()} currentNode = ${q(node).closest('[instanceof Neos.Neos:Test.DocumentType2a]').get()} @process.render = Neos.Neos:Test.RenderNodesDataStructure } @@ -196,7 +227,7 @@ Feature: Tests for the "Neos.ContentRepository" Flow Query methods. When I execute the following Fusion code: """fusion test = Neos.Fusion:DataStructure { - filterSite = ${q([documentNode, node, site]).filter('[instanceof Neos.Neos:Test.Site]').get()} + filterSite = ${q([documentNode, node, site]).filter('[instanceof Neos.Neos:Site]').get()} filterDocument = ${q([documentNode, node, site]).filter('[instanceof Neos.Neos:Document]').get()} filterProperty = ${q([documentNode, node, site]).filter('[uriPathSegment="a1a4"]').get()} @process.render = Neos.Neos:Test.RenderNodesDataStructure From 8e25777c37008214beb69251beb6b3a39477c383 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Fri, 3 Nov 2023 15:42:53 +0100 Subject: [PATCH 02/11] TASK: Fix ParentsUntil Operation and add tests for `unique` and `remove` --- .../ParentsUntilOperation.php | 39 ++++++++----------- .../Domain/Service/NodeTypeNameFactory.php | 5 +++ .../Features/Fusion/FlowQuery.feature | 26 +++++++++++++ 3 files changed, 47 insertions(+), 23 deletions(-) diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ParentsUntilOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ParentsUntilOperation.php index a3501a0d9e0..726215ccabb 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ParentsUntilOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ParentsUntilOperation.php @@ -11,12 +11,16 @@ * source code. */ +use Neos\ContentRepository\Core\NodeType\NodeTypeNames; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindAncestorNodesFilter; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\NodeType\NodeTypeCriteria; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\Nodes; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Eel\FlowQuery\FlowQuery; use Neos\Eel\FlowQuery\Operations\AbstractOperation; use Neos\Flow\Annotations as Flow; +use Neos\Neos\Domain\Service\NodeTypeNameFactory; /** * "parentsUntil" operation working on ContentRepository nodes. It iterates over all @@ -69,18 +73,23 @@ public function evaluate(FlowQuery $flowQuery, array $arguments) { $output = []; $outputNodeAggregateIds = []; + $findAncestorNodesFilter = FindAncestorNodesFilter::create( + NodeTypeCriteria::createWithDisallowedNodeTypeNames( + NodeTypeNames::with(NodeTypeNameFactory::forRoot()) + ) + ); foreach ($flowQuery->getContext() as $contextNode) { - $parentNodes = $this->getParents($contextNode); + $parentNodes = $this->contentRepositoryRegistry->subgraphForNode($contextNode) + ->findAncestorNodes($contextNode->nodeAggregateId, $findAncestorNodesFilter); if (isset($arguments[0]) && !empty($arguments[0] && !$parentNodes->isEmpty())) { - $untilQuery = new FlowQuery([$parentNodes->first()]); - $untilQuery->pushOperation('closest', [$arguments[0]]); - $until = $untilQuery->getContext(); + $filterQuery = new FlowQuery(iterator_to_array($parentNodes)); + $filterQuery->pushOperation('filter', [$arguments[0]]); + $filteredParents = Nodes::fromArray(iterator_to_array($filterQuery)); } - if (isset($until) && is_array($until) && !empty($until) && isset($until[0])) { - $parentNodes = $parentNodes->until($until[0]); + if (isset($filteredParents) && $filteredParents instanceof Nodes && !$filteredParents->isEmpty()) { + $parentNodes = $parentNodes->previousAll($filteredParents->first()); } - foreach ($parentNodes as $parentNode) { if ($parentNode !== null && !isset($outputNodeAggregateIds[$parentNode->nodeAggregateId->value])) { @@ -96,20 +105,4 @@ public function evaluate(FlowQuery $flowQuery, array $arguments) $flowQuery->pushOperation('filter', $arguments[1]); } } - - protected function getParents(Node $contextNode): Nodes - { - $ancestors = []; - $node = $contextNode; - do { - $node = $this->contentRepositoryRegistry->subgraphForNode($node) - ->findParentNode($node->nodeAggregateId); - if ($node === null) { - // no parent found - break; - } - $ancestors[] = $node; - } while (true); - return Nodes::fromArray($ancestors); - } } diff --git a/Neos.Neos/Classes/Domain/Service/NodeTypeNameFactory.php b/Neos.Neos/Classes/Domain/Service/NodeTypeNameFactory.php index 6af5e3827b1..e57bccc7243 100644 --- a/Neos.Neos/Classes/Domain/Service/NodeTypeNameFactory.php +++ b/Neos.Neos/Classes/Domain/Service/NodeTypeNameFactory.php @@ -62,4 +62,9 @@ public static function forSites(): NodeTypeName { return NodeTypeName::fromString(self::NAME_SITES); } + + public static function forRoot(): NodeTypeName + { + return NodeTypeName::fromString(NodeTypeName::ROOT_NODE_TYPE_NAME); + } } diff --git a/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature b/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature index aa6447e947a..e510b679890 100644 --- a/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature +++ b/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature @@ -302,3 +302,29 @@ Feature: Tests for the "Neos.ContentRepository" Flow Query methods. noFilter: a1a5,a1a6,a1a7 withFilter: a1a5,a1a6 """ + + Scenario: Unique + When I execute the following Fusion code: + """fusion + test = ${q([node,site,documentNode]).unique().get()} + test.@process.render = Neos.Neos:Test.RenderNodes + """ + Then I expect the following Fusion rendering result: + """ + a1a4,a + """ + + Scenario: Remove + When I execute the following Fusion code: + """fusion + test = Neos.Fusion:DataStructure { + removeNode = ${q([node,site,documentNode]).remove(node).get()} + nothingToRemove = ${q([node,node,node]).remove(site).get()} + @process.render = Neos.Neos:Test.RenderNodesDataStructure + } + """ + Then I expect the following Fusion rendering result: + """ + removeNode: a + nothingToRemove: a1a4,a1a4,a1a4 + """ From 37d901e9d82c751e65d4648b02a53e715ae8b26c Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Fri, 3 Nov 2023 16:54:43 +0100 Subject: [PATCH 03/11] TASK: Fix `NextUntil` and `PrevUntil` and optimize `Next`, `NextAll`, `Prev`, `PrevAll` In addition: - `Nodes::last` method is added - `Nodes::until` is removed because of broken semantic. BetterAlternatives are `previousAll` and `nextAll` - `NodeNameFactory::forRoot` is added - `Nodes::isEmpty` add assertions that `first` and `last` will return not null --- .../Classes/Projection/ContentGraph/Nodes.php | 29 +++++++++------ .../FlowQueryOperations/NextAllOperation.php | 25 ++++--------- .../FlowQueryOperations/NextOperation.php | 24 ++++--------- .../NextUntilOperation.php | 35 +++++------------- .../FlowQueryOperations/PrevAllOperation.php | 25 +++++-------- .../FlowQueryOperations/PrevOperation.php | 29 +++++---------- .../PrevUntilOperation.php | 36 ++++++------------- 7 files changed, 67 insertions(+), 136 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Nodes.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Nodes.php index adb5887378b..a7c863e8a55 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Nodes.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Nodes.php @@ -14,6 +14,8 @@ namespace Neos\ContentRepository\Core\Projection\ContentGraph; +use function Amp\Promise\first; + /** * An immutable, type-safe collection of Node objects * @@ -97,8 +99,18 @@ public function count(): int public function first(): ?Node { if (count($this->nodes) > 0) { - $array = $this->nodes; - return reset($array); + $key = array_key_first($this->nodes); + return $this->nodes[$key]; + } + + return null; + } + + public function last(): ?Node + { + if (count($this->nodes) > 0) { + $key = array_key_last($this->nodes); + return $this->nodes[$key]; } return null; @@ -116,6 +128,10 @@ public function reverse(): self return new self(array_reverse($this->nodes)); } + /** + * @phpstan-assert-if-false !null $this->first() + * @phpstan-assert-if-false !null $this->last() + */ public function isEmpty(): bool { return $this->count() === 0; @@ -183,13 +199,4 @@ public function nextAll(Node $referenceNode): self return new self(array_slice($this->nodes, $referenceNodeIndex + 1)); } - - /** - * Returns all nodes after the given $referenceNode in this set - */ - public function until(Node $referenceNode): self - { - $referenceNodeIndex = $this->getNodeIndex($referenceNode); - return new self(array_slice($this->nodes, $referenceNodeIndex + 1)); - } } diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/NextAllOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/NextAllOperation.php index d644f2d7a69..98c6d8afbdd 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/NextAllOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/NextAllOperation.php @@ -12,6 +12,7 @@ */ use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindChildNodesFilter; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindSucceedingSiblingNodesFilter; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Eel\FlowQuery\FlowQuery; @@ -69,7 +70,12 @@ public function evaluate(FlowQuery $flowQuery, array $arguments) $output = []; $outputNodePaths = []; foreach ($flowQuery->getContext() as $contextNode) { - foreach ($this->getNextForNode($contextNode) as $nextNode) { + $nextNodes = $this->contentRepositoryRegistry->subgraphForNode($contextNode) + ->findSucceedingSiblingNodes( + $contextNode->nodeAggregateId, + FindSucceedingSiblingNodesFilter::create() + ); + foreach ($nextNodes as $nextNode) { if ($nextNode !== null && !isset($outputNodePaths[$nextNode->nodeAggregateId->value])) { $outputNodePaths[$nextNode->nodeAggregateId->value] = true; $output[] = $nextNode; @@ -82,21 +88,4 @@ public function evaluate(FlowQuery $flowQuery, array $arguments) $flowQuery->pushOperation('filter', $arguments); } } - - /** - * @param Node $contextNode The node for which the next node should be found - * @return Nodes The next nodes of $contextNode - */ - protected function getNextForNode(Node $contextNode): Nodes - { - $subgraph = $this->contentRepositoryRegistry->subgraphForNode($contextNode); - - $parentNode = $subgraph->findParentNode($contextNode->nodeAggregateId); - if ($parentNode === null) { - return Nodes::createEmpty(); - } - - return $subgraph->findChildNodes($parentNode->nodeAggregateId, FindChildNodesFilter::create()) - ->nextAll($contextNode); - } } diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/NextOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/NextOperation.php index 797142689be..ba247b9179e 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/NextOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/NextOperation.php @@ -12,6 +12,7 @@ */ use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindChildNodesFilter; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindSucceedingSiblingNodesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Eel\FlowQuery\FlowQuery; @@ -69,7 +70,11 @@ public function evaluate(FlowQuery $flowQuery, array $arguments) $output = []; $outputNodePaths = []; foreach ($flowQuery->getContext() as $contextNode) { - $nextNode = $this->getNextForNode($contextNode); + $nextNode = $this->contentRepositoryRegistry->subgraphForNode($contextNode) + ->findSucceedingSiblingNodes( + $contextNode->nodeAggregateId, + FindSucceedingSiblingNodesFilter::create() + )->first(); if ($nextNode !== null && !isset($outputNodePaths[$nextNode->nodeAggregateId->value])) { $outputNodePaths[$nextNode->nodeAggregateId->value] = true; $output[] = $nextNode; @@ -81,21 +86,4 @@ public function evaluate(FlowQuery $flowQuery, array $arguments) $flowQuery->pushOperation('filter', $arguments); } } - - /** - * @param Node $contextNode The node for which the preceding node should be found - * @return Node|null The following node of $contextNode or NULL - */ - protected function getNextForNode(Node $contextNode): ?Node - { - $subgraph = $this->contentRepositoryRegistry->subgraphForNode($contextNode); - - $parentNode = $subgraph->findParentNode($contextNode->nodeAggregateId); - if ($parentNode === null) { - return null; - } - - return $subgraph->findChildNodes($parentNode->nodeAggregateId, FindChildNodesFilter::create()) - ->next($contextNode); - } } diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/NextUntilOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/NextUntilOperation.php index 5006fdb6222..f017780a129 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/NextUntilOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/NextUntilOperation.php @@ -12,6 +12,7 @@ */ use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindChildNodesFilter; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindSucceedingSiblingNodesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\Nodes; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; @@ -70,22 +71,21 @@ public function evaluate(FlowQuery $flowQuery, array $arguments) { $output = []; $outputNodeIdentifiers = []; - $until = []; foreach ($flowQuery->getContext() as $contextNode) { - $nextNodes = $this->getNextForNode($contextNode); + $nextNodes = $this->contentRepositoryRegistry->subgraphForNode($contextNode) + ->findSucceedingSiblingNodes( + $contextNode->nodeAggregateId, + FindSucceedingSiblingNodesFilter::create() + ); if (isset($arguments[0]) && !empty($arguments[0])) { $untilQuery = new FlowQuery($nextNodes); $untilQuery->pushOperation('filter', [$arguments[0]]); - - $until = $untilQuery->getContext(); + $untilNodes = Nodes::fromArray(iterator_to_array($untilQuery)); } - /** @var array $until */ - - if (isset($until[0]) && !empty($until[0])) { - $nextNodes = $nextNodes->until($until[0]); + if (isset($untilNodes) && !$untilNodes->isEmpty()) { + $nextNodes = $nextNodes->previousAll($untilNodes->first()); } - foreach ($nextNodes as $nextNode) { if ($nextNode !== null && !isset($outputNodeIdentifiers[$nextNode->nodeAggregateId->value])) { @@ -101,21 +101,4 @@ public function evaluate(FlowQuery $flowQuery, array $arguments) $flowQuery->pushOperation('filter', [$arguments[1]]); } } - - /** - * @param Node $contextNode The node for which the next nodes should be found - * @return Nodes The following nodes of $contextNode - */ - protected function getNextForNode(Node $contextNode): Nodes - { - $subgraph = $this->contentRepositoryRegistry->subgraphForNode($contextNode); - - $parentNode = $subgraph->findParentNode($contextNode->nodeAggregateId); - if ($parentNode === null) { - return Nodes::createEmpty(); - } - - return $subgraph->findChildNodes($parentNode->nodeAggregateId, FindChildNodesFilter::create()) - ->nextAll($contextNode); - } } diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PrevAllOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PrevAllOperation.php index 81a1a8afb22..8f735404b31 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PrevAllOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PrevAllOperation.php @@ -12,6 +12,8 @@ */ use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindChildNodesFilter; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindPrecedingSiblingNodesFilter; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindSucceedingSiblingNodesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\Nodes; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; @@ -69,7 +71,12 @@ public function evaluate(FlowQuery $flowQuery, array $arguments) $output = []; $outputNodeAggregateIds = []; foreach ($flowQuery->getContext() as $contextNode) { - foreach ($this->getPrevForNode($contextNode) as $prevNode) { + $prevNodes = $this->contentRepositoryRegistry->subgraphForNode($contextNode) + ->findPrecedingSiblingNodes( + $contextNode->nodeAggregateId, + FindPrecedingSiblingNodesFilter::create() + )->reverse(); + foreach ($prevNodes as $prevNode) { if ($prevNode !== null && !isset($outputNodeAggregateIds[$prevNode->nodeAggregateId->value]) ) { @@ -84,20 +91,4 @@ public function evaluate(FlowQuery $flowQuery, array $arguments) $flowQuery->pushOperation('filter', $arguments); } } - - /** - * @param Node $contextNode The node for which the preceding node should be found - * @return Nodes The preceding nodes of $contextNode - */ - protected function getPrevForNode(Node $contextNode): Nodes - { - $subgraph = $this->contentRepositoryRegistry->subgraphForNode($contextNode); - $parentNode = $subgraph->findParentNode($contextNode->nodeAggregateId); - if ($parentNode === null) { - return Nodes::createEmpty(); - } - - return $subgraph->findChildNodes($parentNode->nodeAggregateId, FindChildNodesFilter::create()) - ->previousAll($contextNode); - } } diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PrevOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PrevOperation.php index c8e885f8393..4de65e99311 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PrevOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PrevOperation.php @@ -12,6 +12,7 @@ */ use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindChildNodesFilter; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindPrecedingSiblingNodesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Eel\FlowQuery\FlowQuery; @@ -68,10 +69,14 @@ public function evaluate(FlowQuery $flowQuery, array $arguments): void $output = []; $outputNodePaths = []; foreach ($flowQuery->getContext() as $contextNode) { - $nextNode = $this->getPrevForNode($contextNode); - if ($nextNode !== null && !isset($outputNodePaths[$nextNode->nodeAggregateId->value])) { - $outputNodePaths[$nextNode->nodeAggregateId->value] = true; - $output[] = $nextNode; + $previousNode = $this->contentRepositoryRegistry->subgraphForNode($contextNode) + ->findPrecedingSiblingNodes( + $contextNode->nodeAggregateId, + FindPrecedingSiblingNodesFilter::create() + )->first(); + if ($previousNode !== null && !isset($outputNodePaths[$previousNode->nodeAggregateId->value])) { + $outputNodePaths[$previousNode->nodeAggregateId->value] = true; + $output[] = $previousNode; } } $flowQuery->setContext($output); @@ -80,20 +85,4 @@ public function evaluate(FlowQuery $flowQuery, array $arguments): void $flowQuery->pushOperation('filter', $arguments); } } - - /** - * @param Node $contextNode The node for which the preceding node should be found - * @return Node|null The preceeding node of $contextNode or NULL - */ - protected function getPrevForNode(Node $contextNode): ?Node - { - $subgraph = $this->contentRepositoryRegistry->subgraphForNode($contextNode); - $parentNode = $subgraph->findParentNode($contextNode->nodeAggregateId); - if ($parentNode === null) { - return null; - } - - return $subgraph->findChildNodes($parentNode->nodeAggregateId, FindChildNodesFilter::create()) - ->previous($contextNode); - } } diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PrevUntilOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PrevUntilOperation.php index fe1dea0f61c..d7d483d7061 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PrevUntilOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PrevUntilOperation.php @@ -13,6 +13,7 @@ */ use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindChildNodesFilter; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindPrecedingSiblingNodesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\Projection\ContentGraph\Nodes; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; @@ -33,7 +34,7 @@ class PrevUntilOperation extends AbstractOperation * * @var string */ - protected static $shortName = 'nextUntil'; + protected static $shortName = 'prevUntil'; /** * {@inheritdoc} @@ -70,22 +71,21 @@ public function evaluate(FlowQuery $flowQuery, array $arguments): void { $output = []; $outputNodeIdentifiers = []; - $until = []; foreach ($flowQuery->getContext() as $contextNode) { - $prevNodes = $this->getPrevForNode($contextNode); + $prevNodes = $this->contentRepositoryRegistry->subgraphForNode($contextNode) + ->findPrecedingSiblingNodes( + $contextNode->nodeAggregateId, + FindPrecedingSiblingNodesFilter::create() + ); if (isset($arguments[0]) && !empty($arguments[0])) { $untilQuery = new FlowQuery($prevNodes); $untilQuery->pushOperation('filter', [$arguments[0]]); - - $until = $untilQuery->getContext(); + $untilNodes = Nodes::fromArray(iterator_to_array($untilQuery)); } - /** @var array $until */ - - if (isset($until[0]) && !empty($until[0])) { - $prevNodes = $prevNodes->until($until[0]); + if (isset($untilNodes) && !$untilNodes->isEmpty()) { + $prevNodes = $prevNodes->previousAll($untilNodes->first())->reverse(); } - foreach ($prevNodes as $prevNode) { if ($prevNode !== null && !isset($outputNodeIdentifiers[$prevNode->nodeAggregateId->value])) { @@ -101,20 +101,4 @@ public function evaluate(FlowQuery $flowQuery, array $arguments): void $flowQuery->pushOperation('filter', [$arguments[1]]); } } - - /** - * @param Node $contextNode The node for which the next nodes should be found - * @return Nodes The following nodes of $contextNode - */ - protected function getPrevForNode(Node $contextNode): Nodes - { - $subgraph = $this->contentRepositoryRegistry->subgraphForNode($contextNode); - $parentNode = $subgraph->findParentNode($contextNode->nodeAggregateId); - if ($parentNode === null) { - return Nodes::createEmpty(); - } - - return $subgraph->findChildNodes($parentNode->nodeAggregateId, FindChildNodesFilter::create()) - ->previousAll($contextNode); - } } From 0763ed8880cdc3352f62e110eb5a5e3d0050b8a5 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Sat, 4 Nov 2023 18:12:39 +0100 Subject: [PATCH 04/11] TASK: Disable the failing parts of `find` operation and adjust the oder expectation of type filter --- .../Tests/Behavior/Features/Fusion/FlowQuery.feature | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature b/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature index 1a5fa84d9dd..f43aac3e675 100644 --- a/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature +++ b/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature @@ -350,15 +350,19 @@ Feature: Tests for the "Neos.ContentRepository" Flow Query methods. """fusion test = Neos.Fusion:DataStructure { typeFilter = ${q(node).find('[instanceof Neos.Neos:Test.DocumentType2]').get()} - combinedFilter = ${q(node).find('[instanceof Neos.Neos:Test.DocumentType2][uriPathSegment*="b1"]').get()} + # combinedFilter = ${q(node).find('[instanceof Neos.Neos:Test.DocumentType2][uriPathSegment*="b1"]').get()} @process.render = Neos.Neos:Test.RenderNodesDataStructure } """ Then I expect the following Fusion rendering result: """ - typeFilter: a1a,a1b1a,a1a2,a1b2,a1a3,a1a4,a1a5,a1a6 - combinedFilter: a1b1a + typeFilter: a1a,a1a2,a1b2,a1a3,a1a4,a1a5,a1a6,a1b1a """ + # @todo Decide wether the changed order in `typeFilter` case is ok + # @todo Fix and re enable `combinedFilter` case + # NOTE: Values from Neos 8.3 for comparison + # typeFilter: a1a,a1b1a,a1a2,a1b2,a1a3,a1a4,a1a5,a1a6 + # combinedFilter: a1b1a Scenario: Unique When I execute the following Fusion code: From 6c334341027e9e94599ffe6747a9db9825fa54f2 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Sat, 4 Nov 2023 18:15:51 +0100 Subject: [PATCH 05/11] Apply suggestions from code review Co-authored-by: Marc Henry Schultz <85400359+mhsdesign@users.noreply.github.com> --- .../Classes/Projection/ContentGraph/Nodes.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Nodes.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Nodes.php index a7c863e8a55..d50e1b2b199 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Nodes.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Nodes.php @@ -14,8 +14,6 @@ namespace Neos\ContentRepository\Core\Projection\ContentGraph; -use function Amp\Promise\first; - /** * An immutable, type-safe collection of Node objects * @@ -129,8 +127,8 @@ public function reverse(): self } /** - * @phpstan-assert-if-false !null $this->first() - * @phpstan-assert-if-false !null $this->last() + * @phpstan-assert-if-true Node $this->first() + * @phpstan-assert-if-true Node $this->last() */ public function isEmpty(): bool { From cbbfea3f3f346836b551dbbfe8bc196419f97fee Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Sat, 4 Nov 2023 18:32:09 +0100 Subject: [PATCH 06/11] TASK: Adjust format of the todo notice in `find` operation --- .../Tests/Behavior/Features/Fusion/FlowQuery.feature | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature b/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature index f43aac3e675..469f65016e8 100644 --- a/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature +++ b/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature @@ -349,7 +349,13 @@ Feature: Tests for the "Neos.ContentRepository" Flow Query methods. When I execute the following Fusion code: """fusion test = Neos.Fusion:DataStructure { + # @todo Decide wether the changed order of the results compared to Neos 8.3 is ok + # Result in Neos 8.3: "typeFilter: a1a,a1b1a,a1a2,a1b2,a1a3,a1a4,a1a5,a1a6" + # Result in Neos 9.0: "typeFilter: a1a,a1a2,a1b2,a1a3,a1a4,a1a5,a1a6,a1b1a" typeFilter = ${q(node).find('[instanceof Neos.Neos:Test.DocumentType2]').get()} + # @todo Fix and re enable `combinedFilter` case + # Result in Neos 8.3: "combinedFilter: a1b1a" + # Result in Neos 9.0: "combinedFilter: a1a,a1a2,a1b2,a1a3,a1a4,a1a5,a1a6,a1b1a" # combinedFilter = ${q(node).find('[instanceof Neos.Neos:Test.DocumentType2][uriPathSegment*="b1"]').get()} @process.render = Neos.Neos:Test.RenderNodesDataStructure } @@ -358,11 +364,6 @@ Feature: Tests for the "Neos.ContentRepository" Flow Query methods. """ typeFilter: a1a,a1a2,a1b2,a1a3,a1a4,a1a5,a1a6,a1b1a """ - # @todo Decide wether the changed order in `typeFilter` case is ok - # @todo Fix and re enable `combinedFilter` case - # NOTE: Values from Neos 8.3 for comparison - # typeFilter: a1a,a1b1a,a1a2,a1b2,a1a3,a1a4,a1a5,a1a6 - # combinedFilter: a1b1a Scenario: Unique When I execute the following Fusion code: From 3c2e461655eaab9120b42b4cc024a229221c4a56 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Sat, 4 Nov 2023 18:46:22 +0100 Subject: [PATCH 07/11] TASK: Fix phpstan type assertation annotations --- .../Classes/Projection/ContentGraph/Nodes.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Nodes.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Nodes.php index d50e1b2b199..97560f5668b 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Nodes.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Nodes.php @@ -127,8 +127,8 @@ public function reverse(): self } /** - * @phpstan-assert-if-true Node $this->first() - * @phpstan-assert-if-true Node $this->last() + * @phpstan-assert-if-false Node $this->first() + * @phpstan-assert-if-false Node $this->last() */ public function isEmpty(): bool { From 45d3bd473fb084d9c93a40a95c7280d03081f408 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Tue, 7 Nov 2023 16:45:28 +0100 Subject: [PATCH 08/11] TASK: Add sortNames and priority to flowQuery operations --- .../BackReferenceNodesOperation.php | 13 +++++++++++++ .../BackReferencesOperation.php | 13 +++++++++++++ .../FlowQueryOperations/NextAllOperation.php | 2 +- .../FlowQueryOperations/NextUntilOperation.php | 2 +- .../FlowQueryOperations/ParentsOperation.php | 2 +- .../FlowQueryOperations/ParentsUntilOperation.php | 2 +- .../ReferenceNodesOperation.php | 13 +++++++++++++ .../ReferencePropertyOperation.php | 13 +++++++++++++ .../FlowQueryOperations/ReferencesOperation.php | 13 +++++++++++++ .../FlowQueryOperations/RemoveOperation.php | 14 ++++++++++++++ .../FlowQueryOperations/UniqueOperation.php | 14 ++++++++++++++ 11 files changed, 97 insertions(+), 4 deletions(-) diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/BackReferenceNodesOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/BackReferenceNodesOperation.php index d6c137d01af..2f8d2a456b8 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/BackReferenceNodesOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/BackReferenceNodesOperation.php @@ -36,6 +36,19 @@ */ final class BackReferenceNodesOperation implements OperationInterface { + /** + * {@inheritdoc} + * + * @var string + */ + protected static $shortName = 'backReferenceNodes'; + + /** + * {@inheritdoc} + * + * @var integer + */ + protected static $priority = 0; /** * @Flow\Inject diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/BackReferencesOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/BackReferencesOperation.php index 4330d0902d8..f6b159b570e 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/BackReferencesOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/BackReferencesOperation.php @@ -42,6 +42,19 @@ */ final class BackReferencesOperation implements OperationInterface { + /** + * {@inheritdoc} + * + * @var string + */ + protected static $shortName = 'backReferences'; + + /** + * {@inheritdoc} + * + * @var integer + */ + protected static $priority = 0; /** * @Flow\Inject diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/NextAllOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/NextAllOperation.php index 98c6d8afbdd..6f8944289fe 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/NextAllOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/NextAllOperation.php @@ -39,7 +39,7 @@ class NextAllOperation extends AbstractOperation * * @var integer */ - protected static $priority = 100; + protected static $priority = 0; /** * @Flow\Inject diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/NextUntilOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/NextUntilOperation.php index f017780a129..54d5df9b017 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/NextUntilOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/NextUntilOperation.php @@ -40,7 +40,7 @@ class NextUntilOperation extends AbstractOperation * * @var integer */ - protected static $priority = 100; + protected static $priority = 0; /** * @Flow\Inject diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ParentsOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ParentsOperation.php index e6689095212..45ec64ed6ea 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ParentsOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ParentsOperation.php @@ -40,7 +40,7 @@ class ParentsOperation extends AbstractOperation * * @var integer */ - protected static $priority = 100; + protected static $priority = 0; /** * @Flow\Inject diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ParentsUntilOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ParentsUntilOperation.php index 726215ccabb..1a830b86130 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ParentsUntilOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ParentsUntilOperation.php @@ -42,7 +42,7 @@ class ParentsUntilOperation extends AbstractOperation * * @var integer */ - protected static $priority = 100; + protected static $priority = 0; /** * @Flow\Inject diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ReferenceNodesOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ReferenceNodesOperation.php index 249a28ca21d..fe8e1890b50 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ReferenceNodesOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ReferenceNodesOperation.php @@ -35,6 +35,19 @@ */ final class ReferenceNodesOperation implements OperationInterface { + /** + * {@inheritdoc} + * + * @var string + */ + protected static $shortName = 'referenceNodes'; + + /** + * {@inheritdoc} + * + * @var integer + */ + protected static $priority = 0; /** * @Flow\Inject diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ReferencePropertyOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ReferencePropertyOperation.php index 6acc86ec061..94dcf618ac5 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ReferencePropertyOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ReferencePropertyOperation.php @@ -28,6 +28,19 @@ */ final class ReferencePropertyOperation implements OperationInterface { + /** + * {@inheritdoc} + * + * @var string + */ + protected static $shortName = 'referenceProperty'; + + /** + * {@inheritdoc} + * + * @var integer + */ + protected static $priority = 0; /** @param array $context */ public function canEvaluate($context): bool diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ReferencesOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ReferencesOperation.php index e4de83ce9e0..60737ce46d4 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ReferencesOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ReferencesOperation.php @@ -40,6 +40,19 @@ */ final class ReferencesOperation implements OperationInterface { + /** + * {@inheritdoc} + * + * @var string + */ + protected static $shortName = 'references'; + + /** + * {@inheritdoc} + * + * @var integer + */ + protected static $priority = 0; /** * @Flow\Inject diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/RemoveOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/RemoveOperation.php index 1b189ddd1e5..639ff6b6ba5 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/RemoveOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/RemoveOperation.php @@ -34,6 +34,20 @@ final class RemoveOperation implements OperationInterface { use CreateNodeHashTrait; + /** + * {@inheritdoc} + * + * @var string + */ + protected static $shortName = 'remove'; + + /** + * {@inheritdoc} + * + * @var integer + */ + protected static $priority = 100; + /** @param array $context */ public function canEvaluate($context): bool { diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/UniqueOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/UniqueOperation.php index 113d6bfd7e9..5384fbd5e61 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/UniqueOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/UniqueOperation.php @@ -35,6 +35,20 @@ final class UniqueOperation implements OperationInterface { use CreateNodeHashTrait; + /** + * {@inheritdoc} + * + * @var string + */ + protected static $shortName = 'unique'; + + /** + * {@inheritdoc} + * + * @var integer + */ + protected static $priority = 100; + /** @param array $context */ public function canEvaluate($context): bool { From 5f288c6f85ada5c94af6ee2bb2fe97af6174bacc Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Fri, 10 Nov 2023 17:31:40 +0100 Subject: [PATCH 09/11] TASK: Add special handling of absolute node pathes since fizzle does not like the new syntax (yet) --- .../FlowQueryOperations/FindOperation.php | 25 ++++++++++++------- .../Features/Fusion/FlowQuery.feature | 10 ++++++++ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindOperation.php index b1efe8d2071..c2543d47b7d 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindOperation.php @@ -42,7 +42,7 @@ * * Example (absolute path): * - * q(node).find('/sites/my-site/home') + * q(node).find('//my-site/home') * * Example (identifier): * @@ -118,17 +118,24 @@ public function evaluate(FlowQuery $flowQuery, array $arguments): void return; } - /** @var Node[] $result */ - $result = []; $selectorAndFilter = $arguments[0]; - $parsedFilter = FizzleParser::parseFilterGroup($selectorAndFilter); - - /** @todo fetch them $elsewhere (fusion runtime?) */ $firstContextNode = reset($contextNodes); assert($firstContextNode instanceof Node); - $contentRepository = $this->contentRepositoryRegistry->get($firstContextNode->subgraphIdentity->contentRepositoryId); + $entryPoints = $this->getEntryPoints($contextNodes); + + // handle absolute node pathes and return early as fizzle cannot parse this syntax + if (preg_match('/^\\/<[A-Za-z0-9\\.]+\\:[A-Za-z0-9\\.]+>(\\/[a-z0-9\\-]+)*$/', $selectorAndFilter)) { + $nodePath = AbsoluteNodePath::tryFromString($selectorAndFilter); + $nodes = $this->addNodesByPath($nodePath, $entryPoints, []); + $flowQuery->setContext($nodes); + return; + } + + /** @var Node[] $result */ + $result = []; + $parsedFilter = FizzleParser::parseFilterGroup($selectorAndFilter); $entryPoints = $this->getEntryPoints($contextNodes); foreach ($parsedFilter['Filters'] as $filter) { $filterResults = []; @@ -146,7 +153,7 @@ public function evaluate(FlowQuery $flowQuery, array $arguments): void if (isset($filter['AttributeFilters']) && $filter['AttributeFilters'][0]['Operator'] === 'instanceof') { $nodeTypeName = NodeTypeName::fromString($filter['AttributeFilters'][0]['Operand']); - $filterResults = $this->addNodesByType($nodeTypeName, $entryPoints, $filterResults, $contentRepository); + $filterResults = $this->addNodesByType($nodeTypeName, $entryPoints, $filterResults); unset($filter['AttributeFilters'][0]); $generatedNodes = true; } @@ -257,7 +264,7 @@ protected function addNodesByPath(NodePath|AbsoluteNodePath $nodePath, array $en * @param array $result * @return array */ - protected function addNodesByType(NodeTypeName $nodeTypeName, array $entryPoints, array $result, ContentRepository $contentRepository): array + protected function addNodesByType(NodeTypeName $nodeTypeName, array $entryPoints, array $result): array { $nodeTypeFilter = NodeTypeCriteria::create(NodeTypeNames::with($nodeTypeName), NodeTypeNames::createEmpty()); foreach ($entryPoints as $entryPoint) { diff --git a/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature b/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature index 469f65016e8..9289758f8bd 100644 --- a/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature +++ b/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature @@ -137,6 +137,7 @@ Feature: Tests for the "Neos.ContentRepository" Flow Query methods. test = Neos.Fusion:DataStructure { noFilter = ${q(node).children().get()} withFilter = ${q(node).children('[instanceof Neos.Neos:Test.DocumentType2]').get()} + withName = ${q(node).children('a1a4').get()} @process.render = Neos.Neos:Test.RenderNodesDataStructure } """ @@ -144,6 +145,7 @@ Feature: Tests for the "Neos.ContentRepository" Flow Query methods. """ noFilter: a1a1,a1a2,a1a3,a1a4,a1a5,a1a6,a1a7 withFilter: a1a2,a1a3,a1a4,a1a5,a1a6 + withName: a1a4 """ Scenario: Has @@ -357,12 +359,20 @@ Feature: Tests for the "Neos.ContentRepository" Flow Query methods. # Result in Neos 8.3: "combinedFilter: a1b1a" # Result in Neos 9.0: "combinedFilter: a1a,a1a2,a1b2,a1a3,a1a4,a1a5,a1a6,a1b1a" # combinedFilter = ${q(node).find('[instanceof Neos.Neos:Test.DocumentType2][uriPathSegment*="b1"]').get()} + identifier = ${q(node).find('#a1b1a').get()} + name = ${q(node).find('a1b').get()} + relativePath = ${q(node).find('a1b/a1b1').get()} + absolutePath = ${q(node).find('//a/a1/a1b').get()} @process.render = Neos.Neos:Test.RenderNodesDataStructure } """ Then I expect the following Fusion rendering result: """ typeFilter: a1a,a1a2,a1b2,a1a3,a1a4,a1a5,a1a6,a1b1a + identifier: a1b1a + name: a1b + relativePath: a1b1 + absolutePath: a1b """ Scenario: Unique From 76d0eb8a1bededbbeab72f62dbb6410830414ec5 Mon Sep 17 00:00:00 2001 From: Martin Ficzel Date: Fri, 10 Nov 2023 17:53:54 +0100 Subject: [PATCH 10/11] TASK: Fix find operation Absolute pathes are handled separately as the fizzle parser cannot handle the syntax --- .../FlowQueryOperations/ChildrenOperation.php | 2 +- .../FlowQueryOperations/FindOperation.php | 83 ++++++++++--------- .../Features/Fusion/FlowQuery.feature | 9 +- 3 files changed, 46 insertions(+), 48 deletions(-) diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ChildrenOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ChildrenOperation.php index c9bfd37be60..46b4e7cbe33 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ChildrenOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/ChildrenOperation.php @@ -195,7 +195,7 @@ protected function earlyOptimizationOfFilters(FlowQuery $flowQuery, array $parse }); $filteredFlowQuery = new FlowQuery($filteredOutput); $filteredFlowQuery->pushOperation('filter', [$attributeFilters]); - $filteredOutput = $filteredFlowQuery->getContext(); + $filteredOutput = iterator_to_array($filteredFlowQuery); } // Add filtered nodes to output diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindOperation.php index c2543d47b7d..2efce7928d1 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindOperation.php @@ -125,53 +125,56 @@ public function evaluate(FlowQuery $flowQuery, array $arguments): void $entryPoints = $this->getEntryPoints($contextNodes); - // handle absolute node pathes and return early as fizzle cannot parse this syntax - if (preg_match('/^\\/<[A-Za-z0-9\\.]+\\:[A-Za-z0-9\\.]+>(\\/[a-z0-9\\-]+)*$/', $selectorAndFilter)) { - $nodePath = AbsoluteNodePath::tryFromString($selectorAndFilter); - $nodes = $this->addNodesByPath($nodePath, $entryPoints, []); - $flowQuery->setContext($nodes); - return; - } - /** @var Node[] $result */ $result = []; - $parsedFilter = FizzleParser::parseFilterGroup($selectorAndFilter); - $entryPoints = $this->getEntryPoints($contextNodes); - foreach ($parsedFilter['Filters'] as $filter) { - $filterResults = []; - $generatedNodes = false; - if (isset($filter['IdentifierFilter'])) { - $nodeAggregateId = NodeAggregateId::fromString($filter['IdentifierFilter']); - $filterResults = $this->addNodesById($nodeAggregateId, $entryPoints, $filterResults); - $generatedNodes = true; - } elseif (isset($filter['PropertyNameFilter']) || isset($filter['PathFilter'])) { - $nodePath = AbsoluteNodePath::tryFromString($filter['PropertyNameFilter'] ?? $filter['PathFilter']) - ?: NodePath::fromString($filter['PropertyNameFilter'] ?? $filter['PathFilter']); - $filterResults = $this->addNodesByPath($nodePath, $entryPoints, $filterResults); - $generatedNodes = true; - } + $selectorAndFilterParts = explode(',', $selectorAndFilter); + foreach ($selectorAndFilterParts as $selectorAndFilterPart) { - if (isset($filter['AttributeFilters']) && $filter['AttributeFilters'][0]['Operator'] === 'instanceof') { - $nodeTypeName = NodeTypeName::fromString($filter['AttributeFilters'][0]['Operand']); - $filterResults = $this->addNodesByType($nodeTypeName, $entryPoints, $filterResults); - unset($filter['AttributeFilters'][0]); - $generatedNodes = true; + // handle absolute node pathes separately as fizzle cannot parse this syntax (yet) + if ($nodePath = AbsoluteNodePath::tryFromString($selectorAndFilterPart)) { + $nodes = $this->addNodesByPath($nodePath, $entryPoints, []); + $result = array_merge($result, $nodes); + continue; } - if (isset($filter['AttributeFilters']) && count($filter['AttributeFilters']) > 0) { - if (!$generatedNodes) { - throw new FlowQueryException( - 'find() needs an identifier, path or instanceof filter for the first filter part', - 1436884196 - ); + + $parsedFilter = FizzleParser::parseFilterGroup($selectorAndFilterPart); + $entryPoints = $this->getEntryPoints($contextNodes); + foreach ($parsedFilter['Filters'] as $filter) { + $filterResults = []; + $generatedNodes = false; + if (isset($filter['IdentifierFilter'])) { + $nodeAggregateId = NodeAggregateId::fromString($filter['IdentifierFilter']); + $filterResults = $this->addNodesById($nodeAggregateId, $entryPoints, $filterResults); + $generatedNodes = true; + } elseif (isset($filter['PropertyNameFilter']) || isset($filter['PathFilter'])) { + $nodePath = AbsoluteNodePath::tryFromString($filter['PropertyNameFilter'] ?? $filter['PathFilter']) + ?: NodePath::fromString($filter['PropertyNameFilter'] ?? $filter['PathFilter']); + $filterResults = $this->addNodesByPath($nodePath, $entryPoints, $filterResults); + $generatedNodes = true; + } + + if (isset($filter['AttributeFilters']) && $filter['AttributeFilters'][0]['Operator'] === 'instanceof') { + $nodeTypeName = NodeTypeName::fromString($filter['AttributeFilters'][0]['Operand']); + $filterResults = $this->addNodesByType($nodeTypeName, $entryPoints, $filterResults); + unset($filter['AttributeFilters'][0]); + $generatedNodes = true; } - $filterQuery = new FlowQuery($filterResults); - foreach ($filter['AttributeFilters'] as $attributeFilter) { - $filterQuery->pushOperation('filter', [$attributeFilter['text']]); + if (isset($filter['AttributeFilters']) && count($filter['AttributeFilters']) > 0) { + if (!$generatedNodes) { + throw new FlowQueryException( + 'find() needs an identifier, path or instanceof filter for the first filter part', + 1436884196 + ); + } + $filterQuery = new FlowQuery($filterResults); + foreach ($filter['AttributeFilters'] as $attributeFilter) { + $filterQuery->pushOperation('filter', [$attributeFilter['text']]); + } + /** @var array $filterResults */ + $filterResults = iterator_to_array($filterQuery); } - /** @var array $filterResults */ - $filterResults = $filterQuery->getContext(); + $result = array_merge($result, $filterResults); } - $result = array_merge($result, $filterResults); } $uniqueResult = []; diff --git a/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature b/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature index 9289758f8bd..07835b41d8a 100644 --- a/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature +++ b/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature @@ -351,14 +351,8 @@ Feature: Tests for the "Neos.ContentRepository" Flow Query methods. When I execute the following Fusion code: """fusion test = Neos.Fusion:DataStructure { - # @todo Decide wether the changed order of the results compared to Neos 8.3 is ok - # Result in Neos 8.3: "typeFilter: a1a,a1b1a,a1a2,a1b2,a1a3,a1a4,a1a5,a1a6" - # Result in Neos 9.0: "typeFilter: a1a,a1a2,a1b2,a1a3,a1a4,a1a5,a1a6,a1b1a" typeFilter = ${q(node).find('[instanceof Neos.Neos:Test.DocumentType2]').get()} - # @todo Fix and re enable `combinedFilter` case - # Result in Neos 8.3: "combinedFilter: a1b1a" - # Result in Neos 9.0: "combinedFilter: a1a,a1a2,a1b2,a1a3,a1a4,a1a5,a1a6,a1b1a" - # combinedFilter = ${q(node).find('[instanceof Neos.Neos:Test.DocumentType2][uriPathSegment*="b1"]').get()} + combinedFilter = ${q(node).find('[instanceof Neos.Neos:Test.DocumentType2][uriPathSegment*="b1"]').get()} identifier = ${q(node).find('#a1b1a').get()} name = ${q(node).find('a1b').get()} relativePath = ${q(node).find('a1b/a1b1').get()} @@ -369,6 +363,7 @@ Feature: Tests for the "Neos.ContentRepository" Flow Query methods. Then I expect the following Fusion rendering result: """ typeFilter: a1a,a1a2,a1b2,a1a3,a1a4,a1a5,a1a6,a1b1a + combinedFilter: a1b1a identifier: a1b1a name: a1b relativePath: a1b1 From b5a0fc77250fa500dcba336cacfce42db28fe771 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Tue, 14 Nov 2023 19:32:47 +0100 Subject: [PATCH 11/11] TASK: Enable behat strict mode to fail tests without snippet implementation --- .composer.json | 17 +++++++++-------- composer.json | 17 +++++++++-------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/.composer.json b/.composer.json index 67054c5642d..78a49896c90 100644 --- a/.composer.json +++ b/.composer.json @@ -30,19 +30,20 @@ "test:functional": [ "../../bin/phpunit --colors --stop-on-failure -c ../../Build/BuildEssentials/PhpUnit/FunctionalTests.xml Neos.ContentRepository.Core/Tests/Functional" ], + "test:behat-cli": "../../bin/behat -f progress --strict --no-interaction", "test:behavioral": [ - "../../bin/behat -f progress -c Neos.ContentRepository.BehavioralTests/Tests/Behavior/behat.yml.dist", - "../../bin/behat -f progress -c Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/behat.yml.dist", + "@test:behat-cli -c Neos.ContentRepository.BehavioralTests/Tests/Behavior/behat.yml.dist", + "@test:behat-cli -c Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/behat.yml.dist", "../../flow doctrine:migrate --quiet; ../../flow cr:setup", - "../../bin/behat -f progress -c Neos.Neos/Tests/Behavior/behat.yml", - "../../bin/behat -f progress -c Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/behat.yml.dist" + "@test:behat-cli -c Neos.Neos/Tests/Behavior/behat.yml", + "@test:behat-cli -c Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/behat.yml.dist" ], "test:behavioral:stop-on-failure": [ - "../../bin/behat -vvv --stop-on-failure -f progress -c Neos.ContentRepository.BehavioralTests/Tests/Behavior/behat.yml.dist", - "../../bin/behat -vvv --stop-on-failure -f progress -c Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/behat.yml.dist", + "@test:behat-cli -vvv --stop-on-failure -c Neos.ContentRepository.BehavioralTests/Tests/Behavior/behat.yml.dist", + "@test:behat-cli -vvv --stop-on-failure -c Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/behat.yml.dist", "../../flow doctrine:migrate --quiet; ../../flow cr:setup", - "../../bin/behat -vvv --stop-on-failure -f progress -c Neos.Neos/Tests/Behavior/behat.yml", - "../../bin/behat -vvv --stop-on-failure -f progress -c Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/behat.yml.dist" + "@test:behat-cli -vvv --stop-on-failure -c Neos.Neos/Tests/Behavior/behat.yml", + "@test:behat-cli -vvv --stop-on-failure -c Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/behat.yml.dist" ], "test": [ "@test:unit", diff --git a/composer.json b/composer.json index e5bcc219ed7..bf92a8e4a11 100644 --- a/composer.json +++ b/composer.json @@ -111,19 +111,20 @@ "test:functional": [ "../../bin/phpunit --colors --stop-on-failure -c ../../Build/BuildEssentials/PhpUnit/FunctionalTests.xml Neos.ContentRepository.Core/Tests/Functional" ], + "test:behat-cli": "../../bin/behat -vvv -f progress --strict --no-interaction", "test:behavioral": [ - "../../bin/behat -f progress -c Neos.ContentRepository.BehavioralTests/Tests/Behavior/behat.yml.dist", - "../../bin/behat -f progress -c Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/behat.yml.dist", + "@test:behat-cli -c Neos.ContentRepository.BehavioralTests/Tests/Behavior/behat.yml.dist", + "@test:behat-cli -c Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/behat.yml.dist", "../../flow doctrine:migrate --quiet; ../../flow cr:setup", - "../../bin/behat -f progress -c Neos.Neos/Tests/Behavior/behat.yml", - "../../bin/behat -f progress -c Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/behat.yml.dist" + "@test:behat-cli -c Neos.Neos/Tests/Behavior/behat.yml", + "@test:behat-cli -c Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/behat.yml.dist" ], "test:behavioral:stop-on-failure": [ - "../../bin/behat -vvv --stop-on-failure -f progress -c Neos.ContentRepository.BehavioralTests/Tests/Behavior/behat.yml.dist", - "../../bin/behat -vvv --stop-on-failure -f progress -c Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/behat.yml.dist", + "@test:behat-cli --stop-on-failure -c Neos.ContentRepository.BehavioralTests/Tests/Behavior/behat.yml.dist", + "@test:behat-cli --stop-on-failure -c Neos.ContentGraph.DoctrineDbalAdapter/Tests/Behavior/behat.yml.dist", "../../flow doctrine:migrate --quiet; ../../flow cr:setup", - "../../bin/behat -vvv --stop-on-failure -f progress -c Neos.Neos/Tests/Behavior/behat.yml", - "../../bin/behat -vvv --stop-on-failure -f progress -c Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/behat.yml.dist" + "@test:behat-cli --stop-on-failure -c Neos.Neos/Tests/Behavior/behat.yml", + "@test:behat-cli --stop-on-failure -c Neos.ContentRepository.LegacyNodeMigration/Tests/Behavior/behat.yml.dist" ], "test": [ "@test:unit",