diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FilterOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FilterOperation.php index 7330664de5f..c03a8cc3611 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FilterOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FilterOperation.php @@ -19,7 +19,6 @@ use Neos\Eel\FlowQuery\FlowQuery; use Neos\Flow\Annotations as Flow; use Neos\Neos\Utility\NodeTypeWithFallbackProvider; -use Neos\Utility\ObjectAccess; /** * This filter implementation contains specific behavior for use on ContentRepository @@ -122,14 +121,7 @@ protected function matchesIdentifierFilter($element, $identifier) */ protected function getPropertyPath($element, $propertyPath) { - if ($propertyPath === '_identifier') { - // TODO: deprecated (Neos <9 case) - return $element->aggregateId->value; - } elseif ($propertyPath[0] === '_') { - return ObjectAccess::getPropertyPath($element, substr($propertyPath, 1)); - } else { - return $element->getProperty($propertyPath); - } + return $element->getProperty($propertyPath); } /** diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PropertyOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PropertyOperation.php index 2b44d0b922f..9c0415ddd43 100644 --- a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PropertyOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/PropertyOperation.php @@ -24,12 +24,9 @@ use Neos\Eel\FlowQuery\Operations\AbstractOperation; use Neos\Flow\Annotations as Flow; use Neos\Neos\Utility\NodeTypeWithFallbackProvider; -use Neos\Utility\ObjectAccess; /** - * Used to access properties of a ContentRepository Node. If the property mame is - * prefixed with _, internal node properties like start time, end time, - * hidden are accessed. + * Used to access properties of a ContentRepository Node. */ class PropertyOperation extends AbstractOperation { @@ -84,7 +81,7 @@ public function canEvaluate($context): bool public function evaluate(FlowQuery $flowQuery, array $arguments): mixed { if (empty($arguments[0])) { - throw new FlowQueryException('property() does not support returning all attributes yet', 1332492263); + throw new FlowQueryException(static::$shortName . '() does not allow returning all properties.', 1332492263); } /** @var array $context */ $context = $flowQuery->getContext(); @@ -96,22 +93,8 @@ public function evaluate(FlowQuery $flowQuery, array $arguments): mixed /* @var $element Node */ $element = $context[0]; - if ($propertyName === '_path') { - $subgraph = $this->contentRepositoryRegistry->subgraphForNode($element); - $ancestors = $subgraph->findAncestorNodes( - $element->aggregateId, - FindAncestorNodesFilter::create() - )->reverse(); - - return AbsoluteNodePath::fromLeafNodeAndAncestors($element, $ancestors)->serializeToString(); - } - if ($propertyName === '_identifier') { - // TODO: deprecated (Neos <9 case) - return $element->aggregateId->value; - } - - if ($propertyName[0] === '_') { - return ObjectAccess::getPropertyPath($element, substr($propertyName, 1)); + if ($element->hasProperty($propertyName)) { + return $element->getProperty($propertyName); } $contentRepository = $this->contentRepositoryRegistry->get($element->contentRepositoryId); @@ -136,6 +119,6 @@ public function evaluate(FlowQuery $flowQuery, array $arguments): mixed return $references; } - return $element->getProperty($propertyName); + return null; } } diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/SortByTimestampOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/SortByTimestampOperation.php new file mode 100644 index 00000000000..b49e014694f --- /dev/null +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/SortByTimestampOperation.php @@ -0,0 +1,102 @@ + $arguments the arguments for this operation. + * @return void + * @throws FlowQueryException + */ + public function evaluate(FlowQuery $flowQuery, array $arguments) + { + /** @var array|Node[] $nodes */ + $nodes = $flowQuery->getContext(); + + $sortedNodes = []; + $sortSequence = []; + $nodesByIdentifier = []; + + // Determine the property value to sort by + foreach ($nodes as $node) { + $timeStamp = match($arguments[0] ?? null) { + 'created' => $node->timestamps->created->getTimestamp(), + 'lastModified' => $node->timestamps->lastModified?->getTimestamp(), + 'originalCreated' => $node->timestamps->originalCreated->getTimestamp(), + 'originalLastModified' => $node->timestamps->originalLastModified?->getTimestamp(), + default => throw new FlowQueryException('Please provide a timestamp (created, lastModified, originalLastModified) to sort by.', 1727367726) + }; + + $sortSequence[$node->aggregateId->value] = $timeStamp; + $nodesByIdentifier[$node->aggregateId->value] = $node; + } + + $sortOrder = is_string($arguments[1] ?? null) ? strtoupper($arguments[1]) : null; + if ($sortOrder === 'DESC') { + arsort($sortSequence); + } elseif ($sortOrder === 'ASC') { + asort($sortSequence); + } else { + throw new FlowQueryException('Please provide a valid sort direction (ASC or DESC)', 1727367837); + } + + // Build the sorted context that is returned + foreach ($sortSequence as $nodeIdentifier => $value) { + $sortedNodes[] = $nodesByIdentifier[$nodeIdentifier]; + } + + $flowQuery->setContext($sortedNodes); + } +} diff --git a/Neos.Neos/Classes/Eel/FlowQueryOperations/SortOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/SortOperation.php similarity index 89% rename from Neos.Neos/Classes/Eel/FlowQueryOperations/SortOperation.php rename to Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/SortOperation.php index a2958005f6e..830e3f988c3 100644 --- a/Neos.Neos/Classes/Eel/FlowQueryOperations/SortOperation.php +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/SortOperation.php @@ -12,7 +12,7 @@ declare(strict_types=1); -namespace Neos\Neos\Eel\FlowQueryOperations; +namespace Neos\ContentRepository\NodeAccess\FlowQueryOperations; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\Eel\FlowQuery\FlowQuery; @@ -46,9 +46,7 @@ public function canEvaluate($context) } /** - * {@inheritdoc} - * - * First argument is the node property to sort by. Works with internal arguments (_xyz) as well. + * First argument is the node property to sort by. * Second argument is the sort direction (ASC or DESC). * Third optional argument are the sort options (see https://www.php.net/manual/en/function.sort): * - 'SORT_REGULAR' @@ -113,15 +111,7 @@ public function evaluate(FlowQuery $flowQuery, array $arguments) // Determine the property value to sort by foreach ($nodes as $node) { - if ($sortProperty[0] === '_') { - $propertyValue = \Neos\Utility\ObjectAccess::getPropertyPath($node, substr($sortProperty, 1)); - } else { - $propertyValue = $node->getProperty($sortProperty); - } - - if ($propertyValue instanceof \DateTime) { - $propertyValue = $propertyValue->getTimestamp(); - } + $propertyValue = $node->getProperty($sortProperty); $sortSequence[$node->aggregateId->value] = $propertyValue; $nodesByIdentifier[$node->aggregateId->value] = $node; diff --git a/Neos.ContentRepository.NodeAccess/Tests/Unit/FlowQueryOperations/SortByTimestampOperationTest.php b/Neos.ContentRepository.NodeAccess/Tests/Unit/FlowQueryOperations/SortByTimestampOperationTest.php new file mode 100644 index 00000000000..87914e0e733 --- /dev/null +++ b/Neos.ContentRepository.NodeAccess/Tests/Unit/FlowQueryOperations/SortByTimestampOperationTest.php @@ -0,0 +1,48 @@ +expectException(FlowQueryException::class); + $flowQuery = new \Neos\Eel\FlowQuery\FlowQuery([]); + $operation = new SortByTimestampOperation(); + $operation->evaluate($flowQuery, []); + } + + /** + * @test+ + */ + public function callWithoutWrongTimeStampArgumentsCausesException() + { + $this->expectException(FlowQueryException::class); + $flowQuery = new \Neos\Eel\FlowQuery\FlowQuery([]); + $operation = new SortByTimestampOperation(); + $operation->evaluate($flowQuery, ['erstellt']); + } + + /** + * @test + */ + public function invalidSortDirectionCausesException() + { + $this->expectException(FlowQueryException::class); + $flowQuery = new \Neos\Eel\FlowQuery\FlowQuery([]); + $operation = new SortByTimestampOperation(); + $operation->evaluate($flowQuery, ['created', 'FOO']); + } +} diff --git a/Neos.ContentRepository.NodeAccess/Tests/Unit/FlowQueryOperations/SortOperationTest.php b/Neos.ContentRepository.NodeAccess/Tests/Unit/FlowQueryOperations/SortOperationTest.php new file mode 100644 index 00000000000..a3bb5276452 --- /dev/null +++ b/Neos.ContentRepository.NodeAccess/Tests/Unit/FlowQueryOperations/SortOperationTest.php @@ -0,0 +1,48 @@ +expectException(FlowQueryException::class); + $flowQuery = new \Neos\Eel\FlowQuery\FlowQuery([]); + $operation = new SortOperation(); + $operation->evaluate($flowQuery, []); + } + + /** + * @test + */ + public function invalidSortDirectionCausesException() + { + $this->expectException(FlowQueryException::class); + $flowQuery = new \Neos\Eel\FlowQuery\FlowQuery([]); + $operation = new SortOperation(); + $operation->evaluate($flowQuery, ['title', 'FOO']); + } + + /** + * @test + */ + public function invalidSortOptionCausesException() + { + $this->expectException(FlowQueryException::class); + $flowQuery = new \Neos\Eel\FlowQuery\FlowQuery([]); + $operation = new SortOperation(); + $operation->evaluate($flowQuery, ['title', 'ASC', 'SORT_BAR']); + } +} diff --git a/Neos.Neos/Classes/Fusion/Helper/NodeHelper.php b/Neos.Neos/Classes/Fusion/Helper/NodeHelper.php index 5317ca9a715..e5c83617bb9 100644 --- a/Neos.Neos/Classes/Fusion/Helper/NodeHelper.php +++ b/Neos.Neos/Classes/Fusion/Helper/NodeHelper.php @@ -14,6 +14,7 @@ namespace Neos\Neos\Fusion\Helper; +use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag; use Neos\ContentRepository\Core\NodeType\NodeType; use Neos\ContentRepository\Core\Projection\ContentGraph\AbsoluteNodePath; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; @@ -162,6 +163,11 @@ public function subgraphForNode(Node $node): ContentSubgraphInterface return $this->contentRepositoryRegistry->subgraphForNode($node); } + public function isDisabled(Node $node): bool + { + return $node->tags->contain(SubtreeTag::disabled()); + } + /** * @param string $methodName * @return boolean diff --git a/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature b/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature index 4d4adec15dd..2ab22ae3378 100644 --- a/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature +++ b/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature @@ -51,6 +51,7 @@ Feature: Tests for the "Neos.ContentRepository" Flow Query methods. | Key | Value | | nodeAggregateId | "root" | | nodeTypeName | "Neos.Neos:Sites" | + Given the current date and time is "2024-09-22T12:00:00+01:00" And the following CreateNodeAggregateWithNode commands are executed: | nodeAggregateId | parentNodeAggregateId | nodeTypeName | initialPropertyValues | nodeName | | a | root | Neos.Neos:Site | {"title": "Node a"} | a | @@ -71,6 +72,11 @@ Feature: Tests for the "Neos.ContentRepository" Flow Query methods. | a1b3 | a1b | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1b3", "title": "Node a1b3"} | a1b3 | | a1c | a1 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1c", "title": "Node a1c", "hiddenInMenu": true} | a1c | | a1c1 | a1c | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1c1", "title": "Node a1c1"} | a1c1 | + Given the current date and time is "2024-09-22T18:00:00+01:00" + And the following CreateNodeAggregateWithNode commands are executed: + | nodeAggregateId | parentNodeAggregateId | nodeTypeName | initialPropertyValues | nodeName | + | a2 | a | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a2", "title": "Node a2"} | a2 | + And A site exists for node name "a" and domain "http://localhost" And the sites configuration is: """yaml @@ -395,6 +401,33 @@ Feature: Tests for the "Neos.ContentRepository" Flow Query methods. nothingToRemove: a1a4,a1a4,a1a4 """ + Scenario: Sort + When I execute the following Fusion code: + """fusion + test = Neos.Fusion:DataStructure { + @context { + a2 = ${q(site).find('#a2').get(0)} + a1a1 = ${q(site).find('#a1a1').get(0)} + a1a2 = ${q(site).find('#a1a2').get(0)} + a1a3 = ${q(site).find('#a1a3').get(0)} + a1a4 = ${q(site).find('#a1a4').get(0)} + } + unsorted = ${q([a1a3, a1a4, a1a1, a1a2]).get()} + sortByTitleAsc = ${q([a1a3, a1a4, a1a1, a1a2]).sort("title", "ASC").get()} + sortByUriDesc = ${q([a1a3, a1a4, a1a1, a1a2]).sort("uriPathSegment", "DESC").get()} + # a2 is "older" + sortByDateAsc = ${q([a2, a1a1]).sortByTimestamp("created", "ASC").get()} + @process.render = Neos.Neos:Test.RenderNodesDataStructure + } + """ + Then I expect the following Fusion rendering result: + """ + unsorted: a1a3,a1a4,a1a1,a1a2 + sortByTitleAsc: a1a1,a1a2,a1a3,a1a4 + sortByUriDesc: a1a4,a1a3,a1a2,a1a1 + sortByDateAsc: a1a1,a2 + """ + Scenario: Node accessors (final Node access operations) When the Fusion context node is "a1" When I execute the following Fusion code: diff --git a/Neos.Neos/Tests/Unit/FlowQueryOperations/ParentsOperationTest.php b/Neos.Neos/Tests/Unit/FlowQueryOperations/ParentsOperationTest.php deleted file mode 100644 index 6717fbf0ccd..00000000000 --- a/Neos.Neos/Tests/Unit/FlowQueryOperations/ParentsOperationTest.php +++ /dev/null @@ -1,61 +0,0 @@ -markTestSkipped('TODO - Update with Neos 9.0'); - - $rootNode = $this->createMock(TraversableNode::class); - $sitesNode = $this->createMock(TraversableNode::class); - $siteNode = $this->createMock(TraversableNode::class); - $firstLevelNode = $this->createMock(TraversableNode::class); - $secondLevelNode = $this->createMock(TraversableNode::class); - - $rootNode->expects(self::any())->method('findNodePath')->will(self::returnValue(NodePath::fromString('/'))); - $rootNode->expects(self::any())->method('findParentNode')->will(self::throwException(new NodeException('No parent'))); - $sitesNode->expects(self::any())->method('findNodePath')->will(self::returnValue(NodePath::fromString('/sites'))); - $sitesNode->expects(self::any())->method('findParentNode')->will(self::returnValue($rootNode)); - $siteNode->expects(self::any())->method('findNodePath')->will(self::returnValue(NodePath::fromString('/sites/site'))); - $siteNode->expects(self::any())->method('findParentNode')->will(self::returnValue($sitesNode)); - $firstLevelNode->expects(self::any())->method('findParentNode')->will(self::returnValue($siteNode)); - $firstLevelNode->expects(self::any())->method('findNodePath')->will(self::returnValue(NodePath::fromString('/sites/site/first'))); - $secondLevelNode->expects(self::any())->method('findParentNode')->will(self::returnValue($firstLevelNode)); - $secondLevelNode->expects(self::any())->method('findNodePath')->will(self::returnValue(NodePath::fromString('/sites/site/first/second'))); - - $context = [$secondLevelNode]; - $q = new FlowQuery($context); - - $operation = new ParentsOperation(); - $operation->evaluate($q, []); - - $ancestors = $q->getContext(); - self::assertEquals([$siteNode, $firstLevelNode], $ancestors); - } -}