From 9084d697d132cc14e1fca8243d2d03170b326a30 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 3 Aug 2024 19:58:54 +0200 Subject: [PATCH 1/8] !!! TASK: Move `SearchTerm` to own namespace like other filters --- Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php | 2 +- .../ContentGraph/Filter/CountBackReferencesFilter.php | 2 +- .../Projection/ContentGraph/Filter/CountChildNodesFilter.php | 2 +- .../ContentGraph/Filter/CountDescendantNodesFilter.php | 2 +- .../Projection/ContentGraph/Filter/CountReferencesFilter.php | 2 +- .../Projection/ContentGraph/Filter/FindBackReferencesFilter.php | 2 +- .../Projection/ContentGraph/Filter/FindChildNodesFilter.php | 2 +- .../ContentGraph/Filter/FindDescendantNodesFilter.php | 2 +- .../ContentGraph/Filter/FindPrecedingSiblingNodesFilter.php | 2 +- .../Projection/ContentGraph/Filter/FindReferencesFilter.php | 2 +- .../ContentGraph/Filter/FindSucceedingSiblingNodesFilter.php | 2 +- .../ContentGraph/{ => Filter/SearchTerm}/SearchTerm.php | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) rename Neos.ContentRepository.Core/Classes/Projection/ContentGraph/{ => Filter/SearchTerm}/SearchTerm.php (90%) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php index 407d75ff554..e62ee4b7bd4 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php @@ -22,7 +22,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueLessThan; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueLessThanOrEqual; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueStartsWith; -use Neos\ContentRepository\Core\Projection\ContentGraph\SearchTerm; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\SearchTerm\SearchTerm; use Neos\ContentRepository\Core\SharedModel\Id\UuidFactory; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\PropertyName; diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/CountBackReferencesFilter.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/CountBackReferencesFilter.php index 50573a90d7c..603c56d014c 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/CountBackReferencesFilter.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/CountBackReferencesFilter.php @@ -7,7 +7,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\NodeType\NodeTypeCriteria; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueCriteriaInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\PropertyValueCriteriaParser; -use Neos\ContentRepository\Core\Projection\ContentGraph\SearchTerm; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\SearchTerm\SearchTerm; use Neos\ContentRepository\Core\SharedModel\Node\ReferenceName; /** diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/CountChildNodesFilter.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/CountChildNodesFilter.php index e6bc3188132..8a0574fe12a 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/CountChildNodesFilter.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/CountChildNodesFilter.php @@ -7,7 +7,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\NodeType\NodeTypeCriteria; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueCriteriaInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\PropertyValueCriteriaParser; -use Neos\ContentRepository\Core\Projection\ContentGraph\SearchTerm; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\SearchTerm\SearchTerm; /** * Immutable filter DTO for {@see ContentSubgraphInterface::countChildNodes()} diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/CountDescendantNodesFilter.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/CountDescendantNodesFilter.php index 0a873ae25f2..c86694ab6ff 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/CountDescendantNodesFilter.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/CountDescendantNodesFilter.php @@ -7,7 +7,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\NodeType\NodeTypeCriteria; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueCriteriaInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\PropertyValueCriteriaParser; -use Neos\ContentRepository\Core\Projection\ContentGraph\SearchTerm; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\SearchTerm\SearchTerm; /** * Immutable filter DTO for {@see ContentSubgraphInterface::countDescendantNodes()} diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/CountReferencesFilter.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/CountReferencesFilter.php index b0eb0cf6ecc..17e8ada902a 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/CountReferencesFilter.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/CountReferencesFilter.php @@ -7,7 +7,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\NodeType\NodeTypeCriteria; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueCriteriaInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\PropertyValueCriteriaParser; -use Neos\ContentRepository\Core\Projection\ContentGraph\SearchTerm; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\SearchTerm\SearchTerm; use Neos\ContentRepository\Core\SharedModel\Node\ReferenceName; /** diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/FindBackReferencesFilter.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/FindBackReferencesFilter.php index 72e034046a1..657e92144f9 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/FindBackReferencesFilter.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/FindBackReferencesFilter.php @@ -9,7 +9,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\Pagination\Pagination; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueCriteriaInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\PropertyValueCriteriaParser; -use Neos\ContentRepository\Core\Projection\ContentGraph\SearchTerm; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\SearchTerm\SearchTerm; use Neos\ContentRepository\Core\SharedModel\Node\ReferenceName; /** diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/FindChildNodesFilter.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/FindChildNodesFilter.php index 284dde90eea..66f071cd0d8 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/FindChildNodesFilter.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/FindChildNodesFilter.php @@ -9,7 +9,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\Pagination\Pagination; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueCriteriaInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\PropertyValueCriteriaParser; -use Neos\ContentRepository\Core\Projection\ContentGraph\SearchTerm; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\SearchTerm\SearchTerm; /** * Immutable filter DTO for {@see ContentSubgraphInterface::findChildNodes()} diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/FindDescendantNodesFilter.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/FindDescendantNodesFilter.php index 54277308995..788f3358f7d 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/FindDescendantNodesFilter.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/FindDescendantNodesFilter.php @@ -9,7 +9,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\Pagination\Pagination; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueCriteriaInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\PropertyValueCriteriaParser; -use Neos\ContentRepository\Core\Projection\ContentGraph\SearchTerm; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\SearchTerm\SearchTerm; /** * Immutable filter DTO for {@see ContentSubgraphInterface::findDescendantNodes()} diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/FindPrecedingSiblingNodesFilter.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/FindPrecedingSiblingNodesFilter.php index 7cef184992b..cd92c3bdfc8 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/FindPrecedingSiblingNodesFilter.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/FindPrecedingSiblingNodesFilter.php @@ -9,7 +9,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\Pagination\Pagination; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueCriteriaInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\PropertyValueCriteriaParser; -use Neos\ContentRepository\Core\Projection\ContentGraph\SearchTerm; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\SearchTerm\SearchTerm; /** * Immutable filter DTO for {@see ContentSubgraphInterface::findPrecedingSiblingNodes()} diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/FindReferencesFilter.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/FindReferencesFilter.php index 735c5a42cb0..45b2304d345 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/FindReferencesFilter.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/FindReferencesFilter.php @@ -9,7 +9,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\Pagination\Pagination; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueCriteriaInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\PropertyValueCriteriaParser; -use Neos\ContentRepository\Core\Projection\ContentGraph\SearchTerm; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\SearchTerm\SearchTerm; use Neos\ContentRepository\Core\SharedModel\Node\ReferenceName; /** diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/FindSucceedingSiblingNodesFilter.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/FindSucceedingSiblingNodesFilter.php index b955533c490..e1b70f748f9 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/FindSucceedingSiblingNodesFilter.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/FindSucceedingSiblingNodesFilter.php @@ -9,7 +9,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\Pagination\Pagination; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\Criteria\PropertyValueCriteriaInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\PropertyValue\PropertyValueCriteriaParser; -use Neos\ContentRepository\Core\Projection\ContentGraph\SearchTerm; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\SearchTerm\SearchTerm; /** * Immutable filter DTO for {@see ContentSubgraphInterface::findSucceedingSiblingNodes()} diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/SearchTerm.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTerm.php similarity index 90% rename from Neos.ContentRepository.Core/Classes/Projection/ContentGraph/SearchTerm.php rename to Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTerm.php index 733d3cf7dfd..7d6a807baa7 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/SearchTerm.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTerm.php @@ -12,7 +12,7 @@ declare(strict_types=1); -namespace Neos\ContentRepository\Core\Projection\ContentGraph; +namespace Neos\ContentRepository\Core\Projection\ContentGraph\Filter\SearchTerm; /** * A search term for use in Filters for the {@see ContentSubgraphInterface} API. From 989e7c5a650a58608033f432b5c005befb1a4e20 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 4 Aug 2024 13:18:58 +0200 Subject: [PATCH 2/8] TASK: Add tests asserting the search term database filtering behaviour --- .../Features/NodeTraversal/ChildNodes.feature | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/ChildNodes.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/ChildNodes.feature index 45531933113..9ae1f9eaa73 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/ChildNodes.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/ChildNodes.feature @@ -76,8 +76,9 @@ Feature: Find and count nodes using the findChildNodes and countChildNodes queri | a1 | Neos.ContentRepository.Testing:Page | a | {"text": "a1"} | {} | | a2 | Neos.ContentRepository.Testing:Page | a | {"text": "a2"} | {} | | a2a | Neos.ContentRepository.Testing:SpecialPage | a2 | {"text": "a2a"} | {} | - | a2a1 | Neos.ContentRepository.Testing:Page | a2a | {"text": "a2a1", "stringProperty": "the brown fox", "booleanProperty": true, "integerProperty": 33, "floatProperty": 12.345, "dateProperty": {"__type": "DateTimeImmutable", "value": "1980-12-13"}} | {} | + | a2a1 | Neos.ContentRepository.Testing:Page | a2a | {"text": "a2a1", "stringProperty": "the brown fox likes Äpfel", "booleanProperty": true, "integerProperty": 33, "floatProperty": 12.345, "dateProperty": {"__type": "DateTimeImmutable", "value": "1980-12-13"}} | {} | | a2a2 | Neos.ContentRepository.Testing:Page | a2a | {"text": "a2a2", "stringProperty": "the red fox", "booleanProperty": false, "integerProperty": 22, "floatProperty": 12.34, "dateProperty": {"__type": "DateTimeImmutable", "value": "1980-12-14"}} | {} | + # Note that this node is disabled! See below. | a2a3 | Neos.ContentRepository.Testing:Page | a2a | {"text": "a2a3", "stringProperty": "the red Bear", "integerProperty": 19, "dateProperty": {"__type": "DateTimeImmutable", "value": "1980-12-12"}} | {} | | a2a4 | Neos.ContentRepository.Testing:Page | a2a | {"text": "a2a4", "stringProperty": "the brown Bear", "integerProperty": 19, "dateProperty": {"__type": "DateTimeImmutable", "value": "1980-12-12"}} | {} | | a2a5 | Neos.ContentRepository.Testing:Page | a2a | {"text": "a2a5", "stringProperty": "the brown bear", "integerProperty": 19, "dateProperty": {"__type": "DateTimeImmutable", "value": "1980-12-13"}} | {} | @@ -109,7 +110,15 @@ Feature: Find and count nodes using the findChildNodes and countChildNodes queri # Child nodes filtered by search term When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"searchTerm": "brown"}' I expect the nodes "a2a1,a2a4,a2a5" to be returned + # The total count highlights that the search is case insensitive When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"searchTerm": "bear", "pagination": {"limit": 3, "offset": 1}}' I expect the nodes "a2a5" to be returned and the total count to be 2 + # Case insensitive multibyte search + When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"searchTerm": "äpfel"}' I expect the nodes "a2a1" to be returned + # Search for numbers (could be considered useless) + When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"searchTerm": "22"}' I expect the nodes "a2a2" to be returned + When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"searchTerm": "12.34"}' I expect the nodes "a2a1,a2a2" to be returned + # Search for boolean (could be considered useless) + When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"searchTerm": "true"}' I expect the nodes "a2a1" to be returned # Child nodes paginated When I execute the findChildNodes query for parent node aggregate id "home" and filter '{"pagination": {"limit": 3}}' I expect the nodes "terms,contact,a" to be returned and the total count to be 4 From ef93f949b87f76aca354e2b0922c2582500a1003 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 4 Aug 2024 13:21:28 +0200 Subject: [PATCH 3/8] FEATURE: Introduce `SearchTermMatcher` implemented according to database filtering --- .../Filter/SearchTerm/SearchTermMatcher.php | 44 ++++++ .../SearchTerm/SearchTermMatcherTest.php | 131 ++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcher.php create mode 100644 Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcherTest.php diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcher.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcher.php new file mode 100644 index 00000000000..b24cce52777 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcher.php @@ -0,0 +1,44 @@ +properties->serialized(), $searchTerm); + } + + public static function matchesSerializedPropertyValues(SerializedPropertyValues $serializedPropertyValues, SearchTerm $searchTerm): bool + { + foreach ($serializedPropertyValues as $serializedPropertyValue) { + if (self::matchesSerializedPropertyValue($serializedPropertyValue, $searchTerm)) { + return true; + } + } + return false; + } + + private static function matchesSerializedPropertyValue(SerializedPropertyValue $serializedPropertyValue, SearchTerm $searchTerm): bool + { + return match (true) { + is_string($serializedPropertyValue->value) => mb_stripos($serializedPropertyValue->value, $searchTerm->term) !== false, + // the following behaviour might seem odd, but is implemented after how the database filtering should behave + is_int($serializedPropertyValue->value), + is_float($serializedPropertyValue->value) => str_contains((string)$serializedPropertyValue->value, $searchTerm->term), + $serializedPropertyValue->value === true => $searchTerm->term === 'true', + $serializedPropertyValue->value === false => $searchTerm->term === 'false' + }; + } +} diff --git a/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcherTest.php b/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcherTest.php new file mode 100644 index 00000000000..e3c64afb46f --- /dev/null +++ b/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcherTest.php @@ -0,0 +1,131 @@ + ['brown', self::value('the brown fox')]; + yield 'string found inside string (ci)' => ['BrOWn', self::value('the brown fox')]; + + yield 'string found inside multibyte string (ci)' => ['Äpfel', self::value('Auer schreit der bauer die äpfel sind zu sauer.')]; + + yield 'string matches full string' => ['Sheep', self::value('Sheep')]; + yield 'string matches full string (ci)' => ['sheep', self::value('Sheep')]; + + yield 'string found inside string with special chars' => ['ä b-c+#@', self::value('the example: "ä b-c+#@"')]; + } + + public function matchingNumberLikeComparisonExamples(): iterable + { + yield 'string-number found inside string' => [ + '22', + self::value('feeling like 22 ;)'), + ]; + + yield 'string-number found inside string-number' => [ + '00', + self::value('007'), + ]; + + yield 'string-number found inside int' => [ + '23', + self::value(1234), + ]; + + yield 'string-number found inside float' => [ + '23', + self::value(1234.56), + ]; + + yield 'string-float matches float' => [ + '1234.56', + self::value(1234.56), + ]; + + yield 'string-int matches int' => [ + '0', + self::value(0), + ]; + } + + public function matchingBooleanLikeComparisonExamples(): iterable + { + yield 'string-boolean inside string' => [ + 'true', + self::value('this is true'), + ]; + + yield 'string-true matches boolean' => [ + 'true', + self::value(true), + ]; + + yield 'string-false matches boolean' => [ + 'false', + self::value(false), + ]; + } + + public function notMatchingExamples(): iterable + { + yield 'different chars' => ['aepfel', self::value('äpfel')]; + yield 'upper boolean string representation' => ['TRUE', self::value(true)]; + yield 'string not found inside string' => ['reptv', self::value('eras tour')]; + yield 'integer' => ['0999', self::value(999)]; + yield 'float with comma' => ['12,45', self::value(12.34)]; + } + + /** + * @test + * @dataProvider matchingStringComparisonExamples + * @dataProvider matchingNumberLikeComparisonExamples + * @dataProvider matchingBooleanLikeComparisonExamples + */ + public function searchTermMatchesProperties( + string $searchTerm, + SerializedPropertyValues $properties, + ) { + self::assertTrue( + SearchTermMatcher::matchesSerializedPropertyValues( + $properties, + SearchTerm::fulltext($searchTerm) + ) + ); + } + + /** + * @test + * @dataProvider notMatchingExamples + */ + public function searchTermDoesntMatchesProperties( + string $searchTerm, + SerializedPropertyValues $properties, + ) { + self::assertFalse( + SearchTermMatcher::matchesSerializedPropertyValues( + $properties, + SearchTerm::fulltext($searchTerm) + ) + ); + } + + private static function value(string|bool|float|int $value): SerializedPropertyValues + { + return SerializedPropertyValues::fromArray([ + 'test-property' => SerializedPropertyValue::create( + $value, + gettype($value) + ), + ]); + } +} From 4715f33d474abdd74c77cde4973a8b37ee895318 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 4 Aug 2024 13:42:10 +0200 Subject: [PATCH 4/8] BUGFIX: Consider entry node in nodes endpoint for filtering (used in reference editor) --- .../Classes/Projection/ContentGraph/Nodes.php | 5 ++++ .../Controller/Service/NodesController.php | 24 +++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Nodes.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Nodes.php index 4c4d3688a68..988a35fe361 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Nodes.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Nodes.php @@ -124,6 +124,11 @@ public function merge(self $other): self return self::fromArray($nodes); } + public function append(Node $node): self + { + return new self([...$this->nodes, $node]); + } + public function reverse(): self { return new self(array_reverse($this->nodes)); diff --git a/Neos.Neos/Classes/Controller/Service/NodesController.php b/Neos.Neos/Classes/Controller/Service/NodesController.php index 5d35b1bb1ff..fef2fff8a8e 100644 --- a/Neos.Neos/Classes/Controller/Service/NodesController.php +++ b/Neos.Neos/Classes/Controller/Service/NodesController.php @@ -23,8 +23,11 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\ContentSubgraphInterface; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindChildNodesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindDescendantNodesFilter; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\NodeType\ExpandedNodeTypeCriteria; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\NodeType\NodeTypeCriteria; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\SearchTerm\SearchTermMatcher; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; +use Neos\ContentRepository\Core\Projection\ContentGraph\Nodes; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; @@ -148,16 +151,27 @@ public function indexAction( $entryNode = $subgraph->findNodeByAbsolutePath($nodePath); } - $nodes = !is_null($entryNode) ? $subgraph->findDescendantNodes( - $entryNode->aggregateId, - FindDescendantNodesFilter::create( + $nodes = Nodes::createEmpty(); + if (!is_null($entryNode)) { + $filter = FindDescendantNodesFilter::create( nodeTypes: NodeTypeCriteria::create( NodeTypeNames::fromStringArray($nodeTypes), NodeTypeNames::createEmpty() ), searchTerm: $searchTerm, - ) - ) : []; + ); + if ( + SearchTermMatcher::matchesNode($entryNode, $filter->searchTerm) + && ExpandedNodeTypeCriteria::create($filter->nodeTypes, $contentRepository->getNodeTypeManager()) + ->matches($entryNode->nodeTypeName) + ) { + // include the starting node if it matches + $nodes = $nodes->append($entryNode); + } + $nodes = $nodes->merge( + $subgraph->findDescendantNodes($entryNode->aggregateId, $filter) + ); + } } else { if (!empty($searchTerm)) { throw new \RuntimeException('Combination of $nodeIdentifiers and $searchTerm not supported'); From 3bbabb3576aec44da65856e675fa0ebd1ddb66dd Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sun, 4 Aug 2024 15:18:02 +0200 Subject: [PATCH 5/8] TASK: Introduce array support for SearchTermMatcher --- .../Dto/SerializedPropertyValue.php | 2 - .../Filter/SearchTerm/SearchTermMatcher.php | 24 +++++++---- .../SearchTerm/SearchTermMatcherTest.php | 40 ++++++++++++++++--- 3 files changed, 52 insertions(+), 14 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Dto/SerializedPropertyValue.php b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Dto/SerializedPropertyValue.php index 650652adb12..6df6fcd1d6d 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Dto/SerializedPropertyValue.php +++ b/Neos.ContentRepository.Core/Classes/Feature/NodeModification/Dto/SerializedPropertyValue.php @@ -38,8 +38,6 @@ private function __construct( } /** - * If the value is NULL an unset-property instruction will be returned instead. - * * @param Value $value */ public static function create( diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcher.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcher.php index b24cce52777..4781cff6bf9 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcher.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcher.php @@ -23,22 +23,32 @@ public static function matchesNode(Node $node, SearchTerm $searchTerm): bool public static function matchesSerializedPropertyValues(SerializedPropertyValues $serializedPropertyValues, SearchTerm $searchTerm): bool { foreach ($serializedPropertyValues as $serializedPropertyValue) { - if (self::matchesSerializedPropertyValue($serializedPropertyValue, $searchTerm)) { + if (self::matchesValue($serializedPropertyValue->value, $searchTerm)) { return true; } } return false; } - private static function matchesSerializedPropertyValue(SerializedPropertyValue $serializedPropertyValue, SearchTerm $searchTerm): bool + private static function matchesValue(mixed $value, SearchTerm $searchTerm): bool { + if (is_array($value) || $value instanceof \ArrayObject) { + foreach ($value as $subValue) { + if (self::matchesValue($subValue, $searchTerm)) { + return true; + } + } + return false; + } + return match (true) { - is_string($serializedPropertyValue->value) => mb_stripos($serializedPropertyValue->value, $searchTerm->term) !== false, + is_string($value) => mb_stripos($value, $searchTerm->term) !== false, // the following behaviour might seem odd, but is implemented after how the database filtering should behave - is_int($serializedPropertyValue->value), - is_float($serializedPropertyValue->value) => str_contains((string)$serializedPropertyValue->value, $searchTerm->term), - $serializedPropertyValue->value === true => $searchTerm->term === 'true', - $serializedPropertyValue->value === false => $searchTerm->term === 'false' + is_int($value), + is_float($value) => str_contains((string)$value, $searchTerm->term), + $value === true => $searchTerm->term === 'true', + $value === false => $searchTerm->term === 'false', + default => throw new \InvalidArgumentException(sprintf('Handling for type %s is not implemented.', get_debug_type($value))), }; } } diff --git a/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcherTest.php b/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcherTest.php index e3c64afb46f..212319a15bf 100644 --- a/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcherTest.php +++ b/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcherTest.php @@ -12,7 +12,7 @@ class SearchTermMatcherTest extends TestCase { - public function matchingStringComparisonExamples(): iterable + public static function matchingStringComparisonExamples(): iterable { yield 'string found inside string' => ['brown', self::value('the brown fox')]; yield 'string found inside string (ci)' => ['BrOWn', self::value('the brown fox')]; @@ -25,7 +25,7 @@ public function matchingStringComparisonExamples(): iterable yield 'string found inside string with special chars' => ['ä b-c+#@', self::value('the example: "ä b-c+#@"')]; } - public function matchingNumberLikeComparisonExamples(): iterable + public static function matchingNumberLikeComparisonExamples(): iterable { yield 'string-number found inside string' => [ '22', @@ -58,7 +58,7 @@ public function matchingNumberLikeComparisonExamples(): iterable ]; } - public function matchingBooleanLikeComparisonExamples(): iterable + public static function matchingBooleanLikeComparisonExamples(): iterable { yield 'string-boolean inside string' => [ 'true', @@ -76,6 +76,32 @@ public function matchingBooleanLikeComparisonExamples(): iterable ]; } + public static function matchingArrayComparisonExamples(): iterable + { + // automates the following: + yield 'inside array: string found inside string' => ['foo', self::value(['foo'])]; + yield 'inside array-object: string found inside string' => ['foo', self::value(new \ArrayObject(['foo']))]; + + foreach([ + ...iterator_to_array(self::matchingStringComparisonExamples()), + ...iterator_to_array(self::matchingNumberLikeComparisonExamples()), + ...iterator_to_array(self::matchingBooleanLikeComparisonExamples()), + ] as $name => [$searchTerm, $properties]) { + /** @var SerializedPropertyValues $properties */ + yield 'inside nested array: ' . $name => [$searchTerm, SerializedPropertyValues::fromArray( + array_map( + fn (SerializedPropertyValue $value) => SerializedPropertyValue::create( + // arbitrary deep nested + [[$value->value]], + 'array' + ), + iterator_to_array($properties) + ) + )]; + } + } + + public function notMatchingExamples(): iterable { yield 'different chars' => ['aepfel', self::value('äpfel')]; @@ -83,6 +109,9 @@ public function notMatchingExamples(): iterable yield 'string not found inside string' => ['reptv', self::value('eras tour')]; yield 'integer' => ['0999', self::value(999)]; yield 'float with comma' => ['12,45', self::value(12.34)]; + yield 'array with unmatched string' => ['hello', self::value(['hi'])]; + yield 'array key is not considered matching' => ['key', self::value(['key' => 'foo'])]; + yield 'nested array key is not considered matching' => ['key', self::value([['key' => 'foo']])]; } /** @@ -90,6 +119,7 @@ public function notMatchingExamples(): iterable * @dataProvider matchingStringComparisonExamples * @dataProvider matchingNumberLikeComparisonExamples * @dataProvider matchingBooleanLikeComparisonExamples + * @dataProvider matchingArrayComparisonExamples */ public function searchTermMatchesProperties( string $searchTerm, @@ -119,12 +149,12 @@ public function searchTermDoesntMatchesProperties( ); } - private static function value(string|bool|float|int $value): SerializedPropertyValues + private static function value(string|bool|float|int|array|\ArrayObject $value): SerializedPropertyValues { return SerializedPropertyValues::fromArray([ 'test-property' => SerializedPropertyValue::create( $value, - gettype($value) + '' ), ]); } From 5a8ab499476ee1106829779787cebf5c57a8b836 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Thu, 15 Aug 2024 12:14:18 +0200 Subject: [PATCH 6/8] TASK: Introduce `Nodes::prepend` and cleanup code --- .../Classes/Projection/ContentGraph/Nodes.php | 5 ++++ .../Controller/Service/NodesController.php | 30 ++++++++++--------- phpstan-baseline.neon | 10 +++++++ 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Nodes.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Nodes.php index 988a35fe361..ef55a5f8afe 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Nodes.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Nodes.php @@ -124,6 +124,11 @@ public function merge(self $other): self return self::fromArray($nodes); } + public function prepend(Node $node): self + { + return new self([$node, ...$this->nodes]); + } + public function append(Node $node): self { return new self([...$this->nodes, $node]); diff --git a/Neos.Neos/Classes/Controller/Service/NodesController.php b/Neos.Neos/Classes/Controller/Service/NodesController.php index fef2fff8a8e..5faf919d6ae 100644 --- a/Neos.Neos/Classes/Controller/Service/NodesController.php +++ b/Neos.Neos/Classes/Controller/Service/NodesController.php @@ -25,6 +25,7 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\FindDescendantNodesFilter; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\NodeType\ExpandedNodeTypeCriteria; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\NodeType\NodeTypeCriteria; +use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\SearchTerm\SearchTerm; use Neos\ContentRepository\Core\Projection\ContentGraph\Filter\SearchTerm\SearchTermMatcher; use Neos\ContentRepository\Core\Projection\ContentGraph\NodeAggregate; use Neos\ContentRepository\Core\Projection\ContentGraph\Nodes; @@ -113,6 +114,11 @@ public function indexAction( string $contextNode = null, array|string $nodeIdentifiers = [] ): void { + $searchTerm = $searchTerm === '' ? null : SearchTerm::fulltext($searchTerm); + $nodeTypeCriteria = NodeTypeCriteria::create( + NodeTypeNames::fromStringArray($nodeTypes), + NodeTypeNames::createEmpty() + ); $nodeIds = $nodeIds ?: $nodeIdentifiers; $nodeIds = is_array($nodeIds) ? $nodeIds : [$nodeIds]; $contentRepositoryId = SiteDetectionResult::fromRequest($this->request->getHttpRequest()) @@ -143,6 +149,7 @@ public function indexAction( ); } + $nodes = []; if ($nodeIds === [] && (!is_null($nodeAddress) || !is_null($nodePath))) { if (!is_null($nodeAddress)) { $entryNode = $subgraph->findNodeById($nodeAddress->aggregateId); @@ -151,33 +158,28 @@ public function indexAction( $entryNode = $subgraph->findNodeByAbsolutePath($nodePath); } - $nodes = Nodes::createEmpty(); if (!is_null($entryNode)) { - $filter = FindDescendantNodesFilter::create( - nodeTypes: NodeTypeCriteria::create( - NodeTypeNames::fromStringArray($nodeTypes), - NodeTypeNames::createEmpty() - ), - searchTerm: $searchTerm, + $nodes = $subgraph->findDescendantNodes( + $entryNode->aggregateId, + FindDescendantNodesFilter::create( + nodeTypes: $nodeTypeCriteria, + searchTerm: $searchTerm, + ) ); if ( - SearchTermMatcher::matchesNode($entryNode, $filter->searchTerm) - && ExpandedNodeTypeCriteria::create($filter->nodeTypes, $contentRepository->getNodeTypeManager()) + SearchTermMatcher::matchesNode($entryNode, $searchTerm) + && ExpandedNodeTypeCriteria::create($nodeTypeCriteria, $contentRepository->getNodeTypeManager()) ->matches($entryNode->nodeTypeName) ) { // include the starting node if it matches - $nodes = $nodes->append($entryNode); + $nodes = $nodes->prepend($entryNode); } - $nodes = $nodes->merge( - $subgraph->findDescendantNodes($entryNode->aggregateId, $filter) - ); } } else { if (!empty($searchTerm)) { throw new \RuntimeException('Combination of $nodeIdentifiers and $searchTerm not supported'); } - $nodes = []; foreach ($nodeIds as $nodeAggregateId) { $node = $subgraph->findNodeById( NodeAggregateId::fromString($nodeAggregateId) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 0dd318d2ae9..663b53980ff 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -165,6 +165,16 @@ parameters: count: 1 path: Neos.Neos/Classes/Controller/Module/User/UserSettingsController.php + - + message: "#^Parameter \\#2 \\$searchTerm of static method Neos\\\\ContentRepository\\\\Core\\\\Projection\\\\ContentGraph\\\\Filter\\\\SearchTerm\\\\SearchTermMatcher\\:\\:matchesNode\\(\\) expects Neos\\\\ContentRepository\\\\Core\\\\Projection\\\\ContentGraph\\\\Filter\\\\SearchTerm\\\\SearchTerm, Neos\\\\ContentRepository\\\\Core\\\\Projection\\\\ContentGraph\\\\Filter\\\\SearchTerm\\\\SearchTerm\\|null given\\.$#" + count: 1 + path: Neos.Neos/Classes/Controller/Service/NodesController.php + + - + message: "#^The internal method \"Neos\\\\ContentRepository\\\\Core\\\\Projection\\\\ContentGraph\\\\Filter\\\\NodeType\\\\ExpandedNodeTypeCriteria\\:\\:matches\" is called\\.$#" + count: 1 + path: Neos.Neos/Classes/Controller/Service/NodesController.php + - message: "#^The internal method \"Neos\\\\ContentRepository\\\\Core\\\\DimensionSpace\\\\WeightedDimensionSpacePoint\\:\\:getIdentityHash\" is called\\.$#" count: 3 From 5bce3494734df5535700be6ad9926e3cb4e31208 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Thu, 15 Aug 2024 14:11:51 +0200 Subject: [PATCH 7/8] TASK: Fix boolean behaviour for SearchTermMatcher --- .../ContentGraph/Filter/SearchTerm/SearchTermMatcher.php | 7 +++---- .../Filter/SearchTerm/SearchTermMatcherTest.php | 5 +++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcher.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcher.php index 4781cff6bf9..1afdeb42cad 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcher.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcher.php @@ -4,7 +4,6 @@ namespace Neos\ContentRepository\Core\Projection\ContentGraph\Filter\SearchTerm; -use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValue; use Neos\ContentRepository\Core\Feature\NodeModification\Dto\SerializedPropertyValues; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; @@ -43,11 +42,11 @@ private static function matchesValue(mixed $value, SearchTerm $searchTerm): bool return match (true) { is_string($value) => mb_stripos($value, $searchTerm->term) !== false, - // the following behaviour might seem odd, but is implemented after how the database filtering should behave + // the following behaviour might seem odd, but is implemented after how the doctrine adapter filtering is currently implemented is_int($value), is_float($value) => str_contains((string)$value, $searchTerm->term), - $value === true => $searchTerm->term === 'true', - $value === false => $searchTerm->term === 'false', + $value === true => str_contains('true', $searchTerm->term), + $value === false => str_contains('false', $searchTerm->term), default => throw new \InvalidArgumentException(sprintf('Handling for type %s is not implemented.', get_debug_type($value))), }; } diff --git a/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcherTest.php b/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcherTest.php index 212319a15bf..f320822539c 100644 --- a/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcherTest.php +++ b/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcherTest.php @@ -74,6 +74,11 @@ public static function matchingBooleanLikeComparisonExamples(): iterable 'false', self::value(false), ]; + + yield 'string part matches boolean' => [ + 'ru', + self::value(true), + ]; } public static function matchingArrayComparisonExamples(): iterable From 0190e163c27fecd399720e992ba4cad55e65658f Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Thu, 15 Aug 2024 14:14:38 +0200 Subject: [PATCH 8/8] BUGFIX: Ignore empty search term in subgraph api when filtering --- .../src/NodeQueryBuilder.php | 3 ++ .../Features/NodeTraversal/ChildNodes.feature | 29 +++++++++++-------- .../Filter/SearchTerm/SearchTerm.php | 6 ++++ .../Filter/SearchTerm/SearchTermMatcher.php | 3 ++ .../SearchTerm/SearchTermMatcherTest.php | 7 +++++ .../Controller/Service/NodesController.php | 4 +-- phpstan-baseline.neon | 5 ---- 7 files changed, 38 insertions(+), 19 deletions(-) diff --git a/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php b/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php index e62ee4b7bd4..08848c98681 100644 --- a/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php +++ b/Neos.ContentGraph.DoctrineDbalAdapter/src/NodeQueryBuilder.php @@ -178,6 +178,9 @@ public function addNodeTypeCriteria(QueryBuilder $queryBuilder, ExpandedNodeType public function addSearchTermConstraints(QueryBuilder $queryBuilder, SearchTerm $searchTerm, string $nodeTableAlias = 'n'): void { + if ($searchTerm->term === '') { + return; + } $queryBuilder->andWhere('JSON_SEARCH(' . $nodeTableAlias . '.properties, "one", :searchTermPattern, NULL, "$.*.value") IS NOT NULL')->setParameter('searchTermPattern', '%' . $searchTerm->term . '%'); } diff --git a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/ChildNodes.feature b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/ChildNodes.feature index 9ae1f9eaa73..61e14ccc33c 100644 --- a/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/ChildNodes.feature +++ b/Neos.ContentRepository.BehavioralTests/Tests/Behavior/Features/NodeTraversal/ChildNodes.feature @@ -78,10 +78,12 @@ Feature: Find and count nodes using the findChildNodes and countChildNodes queri | a2a | Neos.ContentRepository.Testing:SpecialPage | a2 | {"text": "a2a"} | {} | | a2a1 | Neos.ContentRepository.Testing:Page | a2a | {"text": "a2a1", "stringProperty": "the brown fox likes Äpfel", "booleanProperty": true, "integerProperty": 33, "floatProperty": 12.345, "dateProperty": {"__type": "DateTimeImmutable", "value": "1980-12-13"}} | {} | | a2a2 | Neos.ContentRepository.Testing:Page | a2a | {"text": "a2a2", "stringProperty": "the red fox", "booleanProperty": false, "integerProperty": 22, "floatProperty": 12.34, "dateProperty": {"__type": "DateTimeImmutable", "value": "1980-12-14"}} | {} | - # Note that this node is disabled! See below. - | a2a3 | Neos.ContentRepository.Testing:Page | a2a | {"text": "a2a3", "stringProperty": "the red Bear", "integerProperty": 19, "dateProperty": {"__type": "DateTimeImmutable", "value": "1980-12-12"}} | {} | + # Note that the node a2a3 is disabled! See below. + | a2a3-disabled | Neos.ContentRepository.Testing:Page | a2a | {"text": "a2a3", "stringProperty": "the red Bear", "integerProperty": 19, "dateProperty": {"__type": "DateTimeImmutable", "value": "1980-12-12"}} | {} | | a2a4 | Neos.ContentRepository.Testing:Page | a2a | {"text": "a2a4", "stringProperty": "the brown Bear", "integerProperty": 19, "dateProperty": {"__type": "DateTimeImmutable", "value": "1980-12-12"}} | {} | | a2a5 | Neos.ContentRepository.Testing:Page | a2a | {"text": "a2a5", "stringProperty": "the brown bear", "integerProperty": 19, "dateProperty": {"__type": "DateTimeImmutable", "value": "1980-12-13"}} | {} | + # Note that the node a2a6 must not contain any properties! + | a2a6-empty | Neos.ContentRepository.Testing:Page | a2a | {} | {} | | b | Neos.ContentRepository.Testing:Page | home | {"text": "b"} | {} | | b1 | Neos.ContentRepository.Testing:Page | b | {"text": "b1"} | {} | And the current date and time is "2023-03-16T13:00:00+01:00" @@ -90,16 +92,19 @@ Feature: Find and count nodes using the findChildNodes and countChildNodes queri | nodeAggregateId | "a2a5" | | propertyValues | {"integerProperty": 20} | And the command DisableNodeAggregate is executed with payload: - | Key | Value | - | nodeAggregateId | "a2a3" | - | nodeVariantSelectionStrategy | "allVariants" | + | Key | Value | + | nodeAggregateId | "a2a3-disabled" | + | nodeVariantSelectionStrategy | "allVariants" | Scenario: # Child nodes without filter When I execute the findChildNodes query for parent node aggregate id "home" I expect the nodes "terms,contact,a,b" to be returned When I execute the findChildNodes query for parent node aggregate id "a" I expect the nodes "a1,a2" to be returned When I execute the findChildNodes query for parent node aggregate id "a1" I expect no nodes to be returned - When I execute the findChildNodes query for parent node aggregate id "a2a" I expect the nodes "a2a1,a2a2,a2a4,a2a5" to be returned + When I execute the findChildNodes query for parent node aggregate id "a2a" I expect the nodes "a2a1,a2a2,a2a4,a2a5,a2a6-empty" to be returned + # Child nodes with empty filter + When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"searchTerm": ""}' I expect the nodes "a2a1,a2a2,a2a4,a2a5,a2a6-empty" to be returned + When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"nodeTypes": ""}' I expect the nodes "a2a1,a2a2,a2a4,a2a5,a2a6-empty" to be returned # Child nodes filtered by node type When I execute the findChildNodes query for parent node aggregate id "home" and filter '{"nodeTypes": "Neos.ContentRepository.Testing:AbstractPage"}' I expect the nodes "terms,contact,a,b" to be returned @@ -149,10 +154,10 @@ Feature: Find and count nodes using the findChildNodes and countChildNodes queri When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"propertyValue": "stringProperty $=~ \"Bear\""}' I expect the nodes "a2a4,a2a5" to be returned # Child nodes with custom ordering - When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"ordering": [{"type": "propertyName", "field": "text", "direction": "ASCENDING"}]}' I expect the nodes "a2a1,a2a2,a2a4,a2a5" to be returned - When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"ordering": [{"type": "propertyName", "field": "text", "direction": "DESCENDING"}]}' I expect the nodes "a2a5,a2a4,a2a2,a2a1" to be returned - When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"ordering": [{"type": "propertyName", "field": "non_existing", "direction": "ASCENDING"}]}' I expect the nodes "a2a1,a2a2,a2a4,a2a5" to be returned - When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"ordering": [{"type": "propertyName", "field": "booleanProperty", "direction": "ASCENDING"}, {"type": "propertyName", "field": "dateProperty", "direction": "ASCENDING"}]}' I expect the nodes "a2a4,a2a5,a2a2,a2a1" to be returned - When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"ordering": [{"type": "timestampField", "field": "CREATED", "direction": "ASCENDING"}]}' I expect the nodes "a2a1,a2a2,a2a4,a2a5" to be returned - When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"ordering": [{"type": "timestampField", "field": "LAST_MODIFIED", "direction": "DESCENDING"}]}' I expect the nodes "a2a5,a2a1,a2a2,a2a4" to be returned + When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"ordering": [{"type": "propertyName", "field": "text", "direction": "ASCENDING"}]}' I expect the nodes "a2a6-empty,a2a1,a2a2,a2a4,a2a5" to be returned + When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"ordering": [{"type": "propertyName", "field": "text", "direction": "DESCENDING"}]}' I expect the nodes "a2a5,a2a4,a2a2,a2a1,a2a6-empty" to be returned + When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"ordering": [{"type": "propertyName", "field": "non_existing", "direction": "ASCENDING"}]}' I expect the nodes "a2a1,a2a2,a2a4,a2a5,a2a6-empty" to be returned + When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"ordering": [{"type": "propertyName", "field": "booleanProperty", "direction": "ASCENDING"}, {"type": "propertyName", "field": "dateProperty", "direction": "ASCENDING"}]}' I expect the nodes "a2a6-empty,a2a4,a2a5,a2a2,a2a1" to be returned + When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"ordering": [{"type": "timestampField", "field": "CREATED", "direction": "ASCENDING"}]}' I expect the nodes "a2a1,a2a2,a2a4,a2a5,a2a6-empty" to be returned + When I execute the findChildNodes query for parent node aggregate id "a2a" and filter '{"ordering": [{"type": "timestampField", "field": "LAST_MODIFIED", "direction": "DESCENDING"}]}' I expect the nodes "a2a5,a2a1,a2a2,a2a4,a2a6-empty" to be returned diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTerm.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTerm.php index 7d6a807baa7..8d1f7b07571 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTerm.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTerm.php @@ -17,6 +17,12 @@ /** * A search term for use in Filters for the {@see ContentSubgraphInterface} API. * + * The search is defined the following: + * - test all properties if one contains the term + * - the term is checked case-insensitive + * - an empty term will lead to no filtering + * - FIXME: define the search behaviour across non-string-typed properties + * * @api DTO for {@see ContentSubgraphInterface} */ final readonly class SearchTerm diff --git a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcher.php b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcher.php index 1afdeb42cad..1d3dff96350 100644 --- a/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcher.php +++ b/Neos.ContentRepository.Core/Classes/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcher.php @@ -21,6 +21,9 @@ public static function matchesNode(Node $node, SearchTerm $searchTerm): bool public static function matchesSerializedPropertyValues(SerializedPropertyValues $serializedPropertyValues, SearchTerm $searchTerm): bool { + if ($searchTerm->term === '') { + return true; + } foreach ($serializedPropertyValues as $serializedPropertyValue) { if (self::matchesValue($serializedPropertyValue->value, $searchTerm)) { return true; diff --git a/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcherTest.php b/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcherTest.php index f320822539c..eaf22358166 100644 --- a/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcherTest.php +++ b/Neos.ContentRepository.Core/Tests/Unit/Projection/ContentGraph/Filter/SearchTerm/SearchTermMatcherTest.php @@ -106,6 +106,12 @@ public static function matchingArrayComparisonExamples(): iterable } } + public function emptySearchTermAlwaysMatches(): iterable + { + yield '1 property' => ['', self::value('foo')]; + yield '1 empty property' => ['', self::value('foo')]; + yield '0 properties' => ['', SerializedPropertyValues::fromArray([])]; + } public function notMatchingExamples(): iterable { @@ -125,6 +131,7 @@ public function notMatchingExamples(): iterable * @dataProvider matchingNumberLikeComparisonExamples * @dataProvider matchingBooleanLikeComparisonExamples * @dataProvider matchingArrayComparisonExamples + * @dataProvider emptySearchTermAlwaysMatches */ public function searchTermMatchesProperties( string $searchTerm, diff --git a/Neos.Neos/Classes/Controller/Service/NodesController.php b/Neos.Neos/Classes/Controller/Service/NodesController.php index 5faf919d6ae..2e5090b31dc 100644 --- a/Neos.Neos/Classes/Controller/Service/NodesController.php +++ b/Neos.Neos/Classes/Controller/Service/NodesController.php @@ -114,7 +114,7 @@ public function indexAction( string $contextNode = null, array|string $nodeIdentifiers = [] ): void { - $searchTerm = $searchTerm === '' ? null : SearchTerm::fulltext($searchTerm); + $searchTerm = SearchTerm::fulltext($searchTerm); $nodeTypeCriteria = NodeTypeCriteria::create( NodeTypeNames::fromStringArray($nodeTypes), NodeTypeNames::createEmpty() @@ -176,7 +176,7 @@ public function indexAction( } } } else { - if (!empty($searchTerm)) { + if ($searchTerm->term !== '') { throw new \RuntimeException('Combination of $nodeIdentifiers and $searchTerm not supported'); } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 663b53980ff..e2c8ce3fc5d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -165,11 +165,6 @@ parameters: count: 1 path: Neos.Neos/Classes/Controller/Module/User/UserSettingsController.php - - - message: "#^Parameter \\#2 \\$searchTerm of static method Neos\\\\ContentRepository\\\\Core\\\\Projection\\\\ContentGraph\\\\Filter\\\\SearchTerm\\\\SearchTermMatcher\\:\\:matchesNode\\(\\) expects Neos\\\\ContentRepository\\\\Core\\\\Projection\\\\ContentGraph\\\\Filter\\\\SearchTerm\\\\SearchTerm, Neos\\\\ContentRepository\\\\Core\\\\Projection\\\\ContentGraph\\\\Filter\\\\SearchTerm\\\\SearchTerm\\|null given\\.$#" - count: 1 - path: Neos.Neos/Classes/Controller/Service/NodesController.php - - message: "#^The internal method \"Neos\\\\ContentRepository\\\\Core\\\\Projection\\\\ContentGraph\\\\Filter\\\\NodeType\\\\ExpandedNodeTypeCriteria\\:\\:matches\" is called\\.$#" count: 1