diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindByCriteriaOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindByCriteriaOperation.php new file mode 100644 index 00000000000..3d00a880b21 --- /dev/null +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindByCriteriaOperation.php @@ -0,0 +1,170 @@ + ". Multiple criteria can be combined using "AND", "OR", "NOT" and "()" + * + * + * property criteria support the following comparison operators: + * + * =~ : Strict equality of case-insensitive value and operand + * = : Strict equality of value and operand + * !=~ : Strict inequality of case-insensitive value and operand + * != : Strict inequality of value and operand + * < : Value is less than operand + * <= : Value is less than or equal to operand + * > : Value is greater than operand + * >= : Value is greater than or equal to operand + * $=~ : Value ends with operand (string-based) or case-insensitive value's last element is equal to operand (array-based) + * $= : Value ends with operand (string-based) or value's last element is equal to operand (array-based) + * ^=~ : Value starts with operand (string-based) or case-insensitive value's first element is equal to operand (array-based) + * ^= : Value starts with operand (string-based) or value's first element is equal to operand (array-based) + * *=~ : Value contains operand (string-based) or case-insensitive value contains an element that is equal to operand (array based) + * *= : Value contains operand (string-based) or value contains an element that is equal to operand (array based) + * + * criteria can be combined using "AND" and "OR": + * + * "prop1 ^= 'foo' AND (prop2 = 'bar' OR prop3 = 'baz')" + * + * furthermore "NOT" can be used to negate a whole sub query + * + * "prop1 ^= 'foo' AND NOT (prop2 = 'bar' OR prop3 = 'baz')" + * + * Argument 3 ({limit?:int, offset?:int}}): Pagination of the date + * + * + * Example (node type): + * + * q(node).findByCriteria('Neos.NodeTypes:Text') + * + * Example (multiple node types): + * + * q(node).findByCriteria('Neos.NodeTypes:Text,Neos.NodeTypes:Image') + * + * Example (node type with property filter): + * + * q(node).findByCriteria('Neos.NodeTypes:Text', 'text*="Neos"') + * + * Example (node type with property filter and pagination): + * + * q(node).findByCriteria('Neos.NodeTypes:Document', 'title*="Flow"', {limit:10, offset:2}) + */ +class FindByCriteriaOperation extends AbstractOperation +{ + use CreateNodeHashTrait; + + /** + * {@inheritdoc} + * + * @var string + */ + protected static $shortName = 'findByCriteria'; + + /** + * {@inheritdoc} + * + * @var integer + */ + protected static $priority = 100; + + /** + * @Flow\Inject + * @var ContentRepositoryRegistry + */ + protected $contentRepositoryRegistry; + + /** + * {@inheritdoc} + * + * @param array $context (or array-like object) onto which this operation should be applied + * @return boolean true if the operation can be applied onto the $context, false otherwise + */ + public function canEvaluate($context) + { + foreach ($context as $contextNode) { + if (!$contextNode instanceof Node) { + return false; + } + } + + return true; + } + /** + * This operation operates rather on the given Context object than on the given node + * and thus may work with the legacy node interface until subgraphs are available + * {@inheritdoc} + * + * @param FlowQuery $flowQuery the FlowQuery object + * @param array $arguments the arguments for this operation + * @throws FlowQueryException + * @throws \Neos\Eel\Exception + * @throws \Neos\Eel\FlowQuery\FizzleException + */ + public function evaluate(FlowQuery $flowQuery, array $arguments): void + { + /** @var array $contextNodes */ + $contextNodes = $flowQuery->getContext(); + if (count($contextNodes) === 0) { + return; + } + + $firstContextNode = reset($contextNodes); + assert($firstContextNode instanceof Node); + + $nodeTypeFilter = $arguments[0] ?? null; + $propertyValueFilter = $arguments[1] ?? null; + $pagination = $arguments[2] ?? null; + + assert($nodeTypeFilter === null || is_string($nodeTypeFilter)); + assert($propertyValueFilter === null || is_string($propertyValueFilter)); + assert($pagination === null || is_array($pagination)); + + /** @var Node[] $result */ + $result = []; + $findDescendentNodesFilter = FindDescendantNodesFilter::create( + nodeTypes: $nodeTypeFilter ? NodeTypeCriteria::fromFilterString($nodeTypeFilter) : null, + propertyValue: $propertyValueFilter ? PropertyValueCriteriaParser::parse($propertyValueFilter) : null, + pagination: $pagination ? Pagination::fromArray($pagination) : null + ); + + /** @var Node $contextNode */ + foreach ($flowQuery->getContext() as $contextNode) { + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($contextNode); + foreach ($subgraph->findDescendantNodes($contextNode->aggregateId, $findDescendentNodesFilter) as $descendant) { + $result[] = $descendant; + } + } + + $flowQuery->setContext($result); + } +} diff --git a/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindByIdentifierOperation.php b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindByIdentifierOperation.php new file mode 100644 index 00000000000..6423a167daa --- /dev/null +++ b/Neos.ContentRepository.NodeAccess/Classes/FlowQueryOperations/FindByIdentifierOperation.php @@ -0,0 +1,111 @@ + $context (or array-like object) onto which this operation should be applied + * @return boolean true if the operation can be applied onto the $context, false otherwise + */ + public function canEvaluate($context) + { + foreach ($context as $contextNode) { + if (!$contextNode instanceof Node) { + return false; + } + } + + return true; + } + /** + * This operation operates rather on the given Context object than on the given node + * and thus may work with the legacy node interface until subgraphs are available + * {@inheritdoc} + * + * @param FlowQuery $flowQuery the FlowQuery object + * @param array $arguments the arguments for this operation + * @throws FlowQueryException + * @throws \Neos\Eel\Exception + * @throws \Neos\Eel\FlowQuery\FizzleException + */ + public function evaluate(FlowQuery $flowQuery, array $arguments): void + { + /** @var array $contextNodes */ + $contextNodes = $flowQuery->getContext(); + if (count($contextNodes) === 0 || empty($arguments[0])) { + return; + } + + $firstContextNode = reset($contextNodes); + assert($firstContextNode instanceof Node); + + $nodeAggregateId = NodeAggregateId::fromString($arguments[0]); + + /** @var Node[] $result */ + $result = []; + + /** @var Node $contextNode */ + foreach ($flowQuery->getContext() as $contextNode) { + $subgraph = $this->contentRepositoryRegistry->subgraphForNode($contextNode); + $nodeByIdentifier = $subgraph->findNodeById($nodeAggregateId); + if ($nodeByIdentifier) { + $result[] = $nodeByIdentifier; + } + } + + $flowQuery->setContext($result); + } +} diff --git a/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature b/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature index 27d4eeffc79..21b435e95ac 100644 --- a/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature +++ b/Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature @@ -375,6 +375,48 @@ Feature: Tests for the "Neos.ContentRepository" Flow Query methods. absolutePath: a1b """ + Scenario: FindByCriteria + When the Fusion context node is "a1" + When I execute the following Fusion code: + """fusion + test = Neos.Fusion:DataStructure { + nodeTypeFilter = ${q(node).findByCriteria('Neos.Neos:Test.DocumentType2').get()} + nodeTypeExcludeFilter = ${q(node).findByCriteria('Neos.Neos:Document,!Neos.Neos:Test.DocumentType1').get()} + nodeTypeCombinedFilter = ${q(node).findByCriteria('Neos.Neos:Test.Document1,Neos.Neos:Test.DocumentType2a').get()} + nodeTypeFilterWithLimit = ${q(node).findByCriteria('Neos.Neos:Test.DocumentType2', null, {limit:2, offset:3}).get()} + propertyFilter = ${q(node).findByCriteria(null, 'uriPathSegment*="b1"').get()} + propertyAndNodeTypeFilter = ${q(node).findByCriteria('Neos.Neos:Test.DocumentType2a', 'uriPathSegment*="b1"').get()} + @process.render = Neos.Neos:Test.RenderNodesDataStructure + } + """ + Then I expect the following Fusion rendering result: + """ + nodeTypeFilter: a1a,a1a2,a1b2,a1a3,a1a4,a1a5,a1a6,a1b1a + nodeTypeExcludeFilter: a1a,a1a2,a1b2,a1a3,a1a4,a1a5,a1a6,a1b1a + nodeTypeCombinedFilter: a1a,a1a2,a1b2,a1a3,a1a4,a1a5,a1a6,a1b1a + nodeTypeFilterWithLimit: a1a3,a1a4 + propertyFilter: a1b1,a1b1a,a1b1b + propertyAndNodeTypeFilter: a1b1a + """ + + Scenario: FindByIdentifier + When the Fusion context node is "a1" + When I execute the following Fusion code: + """fusion + test = Neos.Fusion:DataStructure { + child = ${q(node).findByIdentifier('a1b1').get()} + grandchild = ${q(node).findByIdentifier('a1b1a').get()} + sibling = ${q(node).findByIdentifier('a2').get()} + @process.render = Neos.Neos:Test.RenderNodesDataStructure + } + """ + Then I expect the following Fusion rendering result: + """ + child: a1b1 + grandchild: a1b1a + sibling: a2 + """ + Scenario: Unique When I execute the following Fusion code: """fusion