Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: Add findByCriteria and findByIdentifier flowQuery operation #5435

Draft
wants to merge 1 commit into
base: 9.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
<?php

declare(strict_types=1);

namespace Neos\ContentRepository\NodeAccess\FlowQueryOperations;

/*
* This file is part of the Neos.ContentRepository package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Neos\Flow\Annotations as Flow;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindDescendantNodesFilter;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\NodeType\NodeTypeCriteria;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\Pagination\Pagination;
use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\PropertyValueCriteriaParser;
use Neos\ContentRepository\Core\Projection\ContentGraph\Node;
use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry;
use Neos\Eel\FlowQuery\FlowQuery;
use Neos\Eel\FlowQuery\FlowQueryException;
use Neos\Eel\FlowQuery\Operations\AbstractOperation;

/**
* "findByCriteria" operation working on ContentRepository nodes. This operation allows for retrieval of descendant nodes
*
* Argument 1 (string|null): nodeTypeFilter, A list of NodeType Names seperated by ",", disallowed NodeTypes are prefixed with "!"
*
* Argument 2 (string|null): propertyValueCriteria A property criteria in the form
*
* property criteria are specified as "<property> <operator> <value>". 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<int,mixed> $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<int,mixed> $flowQuery the FlowQuery object
* @param array<int,mixed> $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<int,Node> $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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe in this "next" generation of flowquery we want to forbid the functionality that we iterate over all context nodes and either throw if there are multiple nodes or just always pick the first one. Operating on multiple nodes might not give the expected ordering as one might think

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The one thing that i dislike most is that context can contain anything. Would really like to limit that to nodes

$subgraph = $this->contentRepositoryRegistry->subgraphForNode($contextNode);
foreach ($subgraph->findDescendantNodes($contextNode->aggregateId, $findDescendentNodesFilter) as $descendant) {
$result[] = $descendant;
}
}

$flowQuery->setContext($result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php

declare(strict_types=1);

namespace Neos\ContentRepository\NodeAccess\FlowQueryOperations;

/*
* This file is part of the Neos.ContentRepository package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Neos\Flow\Annotations as Flow;
use Neos\ContentRepository\Core\Projection\ContentGraph\Node;
use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId;
use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry;
use Neos\Eel\FlowQuery\FlowQuery;
use Neos\Eel\FlowQuery\FlowQueryException;
use Neos\Eel\FlowQuery\Operations\AbstractOperation;

/**
* "findByIdentifier" operation working on ContentRepository nodes. This operation allows for retrieval of nodes by identifier
* from the current subgraph
*
* Example:
*
* q(site).findByIdentifier('30e893c1-caef-0ca5-b53d-e5699bb8e506')
*/
class FindByIdentifierOperation extends AbstractOperation
{
use CreateNodeHashTrait;

/**
* {@inheritdoc}
*
* @var string
*/
protected static $shortName = 'findByIdentifier';

/**
* {@inheritdoc}
*
* @var integer
*/
protected static $priority = 100;

/**
* @Flow\Inject
* @var ContentRepositoryRegistry
*/
protected $contentRepositoryRegistry;

/**
* {@inheritdoc}
*
* @param array<int,mixed> $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<int,mixed> $flowQuery the FlowQuery object
* @param array<int,mixed> $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<int,Node> $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);
}
}
42 changes: 42 additions & 0 deletions Neos.Neos/Tests/Behavior/Features/Fusion/FlowQuery.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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.DocumentType1,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,a1b,a1c,a1a1,a1b1,a1c1,a1a3,a1b3,a1a4,a1a5,a1a7,a1b1a,a1b1b
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
Expand Down
Loading