@@ -20,11 +20,13 @@ class QueryMatchFilter extends AbstractFilter
2020 protected const MATCH_QUERY_NEGATION_WRAPPED = '^(?<negate>!)\((?<logicalexpr>.+)\)$ ' ;
2121 protected const MATCH_QUERY_NEGATION_UNWRAPPED = '^(?<negate>!)(?<logicalexpr>.+)$ ' ;
2222 protected const MATCH_QUERY_OPERATORS = '
23- (@\.(?<key>[^\s<>!=]+)|@\[[" \']?(?<keySquare>.*?)[" \']?\]|(?<node>@))
24- (\s*(?<operator>==|=~|=|<>|!==|!=|>=|<=|>|<|in|!in|nin)\s*(?<comparisonValue>.+?(?=(&&|$))))?
25- (\s*(?<logicaland >&&)\s*)?
23+ (@\.(?<key>[^\s<>!=]+)|@\[[" \']?(?<keySquare>.*?)[" \']?\]|(?<node>@)|(%group(?<group>\d+)%) )
24+ (\s*(?<operator>==|=~|=|<>|!==|!=|>=|<=|>|<|in|!in|nin)\s*(?<comparisonValue>.+?(?=(&&|$|\|\||% ))))?
25+ (\s*(?<logicalandor >&&|\|\| )\s*)?
2626 ' ;
2727
28+ protected const MATCH_GROUPED_EXPRESSION = '#\([^)(]*+(?:(?R)[^)(]*)*+\)# ' ;
29+
2830 /**
2931 * @throws JSONPathException
3032 */
@@ -40,6 +42,25 @@ public function filter($collection): array
4042 $ filterExpression = $ negationMatches ['logicalexpr ' ];
4143 }
4244
45+ $ filterGroups = [];
46+ if (
47+ \preg_match_all (
48+ static ::MATCH_GROUPED_EXPRESSION ,
49+ $ filterExpression ,
50+ $ matches ,
51+ PREG_OFFSET_CAPTURE | PREG_UNMATCHED_AS_NULL
52+ )
53+ ) {
54+ foreach ($ matches [0 ] as $ i => $ matchesGroup ) {
55+ $ test = \substr ($ matchesGroup [0 ], 1 , -1 );
56+ //sanity check that our group is a group and not something within a string or regular expression
57+ if (\preg_match ('/ ' . static ::MATCH_QUERY_OPERATORS . '/x ' , $ test )) {
58+ $ filterGroups [$ i ] = $ test ;
59+ $ filterExpression = \str_replace ($ matchesGroup [0 ], "%group $ i% " , $ filterExpression );
60+ }
61+ }
62+ }
63+
4364 $ match = \preg_match_all (
4465 '/ ' . static ::MATCH_QUERY_OPERATORS . '/x ' ,
4566 $ filterExpression ,
@@ -50,18 +71,35 @@ public function filter($collection): array
5071 if (
5172 $ match === false
5273 || !isset ($ matches [1 ][0 ])
53- || isset ($ matches ['logicaland ' ][array_key_last ($ matches ['logicaland ' ])])
74+ || isset ($ matches ['logicalandor ' ][array_key_last ($ matches ['logicalandor ' ])])
5475 ) {
5576 throw new RuntimeException ('Malformed filter query ' );
5677 }
5778
5879 $ return = [];
5980
60- for ($ logicalAndNum = 0 ; $ logicalAndNum < \count ($ matches [0 ]); $ logicalAndNum ++) {
61- $ key = $ matches ['key ' ][$ logicalAndNum ] ?: $ matches ['keySquare ' ][$ logicalAndNum ];
81+ for ($ expressionPart = 0 ; $ expressionPart < \count ($ matches [0 ]); $ expressionPart ++) {
82+ $ filteredCollection = $ collection ;
83+ $ logicalJoin = $ expressionPart > 0 ? $ matches ['logicalandor ' ][$ expressionPart - 1 ] : null ;
84+ if ($ logicalJoin === '&& ' ) {
85+ //Restrict the nodes we need to look at to those already meeting criteria
86+ $ filteredCollection = $ return ;
87+ $ return = [];
88+ }
89+
90+ //Processing a group
91+ if ($ matches ['group ' ][$ expressionPart ] !== null ) {
92+ $ filter = '$[?( ' . $ filterGroups [$ matches ['group ' ][$ expressionPart ]] . ')] ' ;
93+ $ resolve = (new JSONPath ($ filteredCollection ))->find ($ filter )->getData ();
94+ $ return = $ resolve ;
95+ continue ;
96+ }
6297
63- $ operator = $ matches ['operator ' ][$ logicalAndNum ] ?? null ;
64- $ comparisonValue = $ matches ['comparisonValue ' ][$ logicalAndNum ] ?? null ;
98+ //Process a normal expression
99+ $ key = $ matches ['key ' ][$ expressionPart ] ?: $ matches ['keySquare ' ][$ expressionPart ];
100+
101+ $ operator = $ matches ['operator ' ][$ expressionPart ] ?? null ;
102+ $ comparisonValue = $ matches ['comparisonValue ' ][$ expressionPart ] ?? null ;
65103
66104 if (\is_string ($ comparisonValue )) {
67105 $ comparisonValue = \preg_replace ('/^[ \']/ ' , '" ' , $ comparisonValue );
@@ -73,46 +111,45 @@ public function filter($collection): array
73111 }
74112 }
75113
76- $ filteredCollection = $ collection ;
77- if ($ logicalAndNum > 0 ) {
78- $ filteredCollection = $ return ;
79- $ return = [];
80- }
81-
82- foreach ($ filteredCollection as $ value ) {
83- $ value1 = null ;
114+ foreach ($ filteredCollection as $ nodeIndex => $ node ) {
115+ if ($ logicalJoin === '|| ' && \array_key_exists ($ nodeIndex , $ return )) {
116+ //Short-circuit, node already exists in output due to previous test
117+ continue ;
118+ }
119+ $ selectedNode = null ;
84120
85- $ notNothing = AccessHelper::keyExists ($ value , $ key , $ this ->magicIsAllowed );
121+ $ notNothing = AccessHelper::keyExists ($ node , $ key , $ this ->magicIsAllowed );
86122 if ($ key ) {
87123 if ($ notNothing ) {
88- $ value1 = AccessHelper::getValue ($ value , $ key , $ this ->magicIsAllowed );
124+ $ selectedNode = AccessHelper::getValue ($ node , $ key , $ this ->magicIsAllowed );
89125 } elseif (\str_contains ($ key , '. ' )) {
90- $ foundValue = (new JSONPath ($ value ))->find ($ key )->getData ();
126+ $ foundValue = (new JSONPath ($ node ))->find ($ key )->getData ();
91127 if ($ foundValue ) {
92- $ value1 = $ foundValue [0 ];
128+ $ selectedNode = $ foundValue [0 ];
93129 $ notNothing = true ;
94130 }
95131 }
96132 } else {
97- $ value1 = $ value ;
133+ //Node selection was plain @
134+ $ selectedNode = $ node ;
98135 $ notNothing = true ;
99136 }
100137
101138 $ comparisonResult = null ;
102139 if ($ notNothing ) {
103140 $ comparisonResult = match ($ operator ) {
104- null => AccessHelper::keyExists ($ value , $ key , $ this ->magicIsAllowed ) || (!$ key ),
105- "= " , "== " => $ this ->compareEquals ($ value1 , $ comparisonValue ),
106- "!= " , "!== " , "<> " => !$ this ->compareEquals ($ value1 , $ comparisonValue ),
107- '=~ ' => @\preg_match ($ comparisonValue , $ value1 ),
108- '< ' => $ this ->compareLessThan ($ value1 , $ comparisonValue ),
109- '<= ' => $ this ->compareLessThan ($ value1 , $ comparisonValue )
110- || $ this ->compareEquals ($ value1 , $ comparisonValue ),
111- '> ' => $ this ->compareLessThan ($ comparisonValue , $ value1 ), //rfc semantics
112- '>= ' => $ this ->compareLessThan ($ comparisonValue , $ value1 ) //rfc semantics
113- || $ this ->compareEquals ($ value1 , $ comparisonValue ),
114- "in " => \is_array ($ comparisonValue ) && \in_array ($ value1 , $ comparisonValue , true ),
115- 'nin ' , "!in " => \is_array ($ comparisonValue ) && !\in_array ($ value1 , $ comparisonValue , true )
141+ null => AccessHelper::keyExists ($ node , $ key , $ this ->magicIsAllowed ) || (!$ key ),
142+ "= " , "== " => $ this ->compareEquals ($ selectedNode , $ comparisonValue ),
143+ "!= " , "!== " , "<> " => !$ this ->compareEquals ($ selectedNode , $ comparisonValue ),
144+ '=~ ' => @\preg_match ($ comparisonValue , $ selectedNode ),
145+ '< ' => $ this ->compareLessThan ($ selectedNode , $ comparisonValue ),
146+ '<= ' => $ this ->compareLessThan ($ selectedNode , $ comparisonValue )
147+ || $ this ->compareEquals ($ selectedNode , $ comparisonValue ),
148+ '> ' => $ this ->compareLessThan ($ comparisonValue , $ selectedNode ), //rfc semantics
149+ '>= ' => $ this ->compareLessThan ($ comparisonValue , $ selectedNode ) //rfc semantics
150+ || $ this ->compareEquals ($ selectedNode , $ comparisonValue ),
151+ "in " => \is_array ($ comparisonValue ) && \in_array ($ selectedNode , $ comparisonValue , true ),
152+ 'nin ' , "!in " => \is_array ($ comparisonValue ) && !\in_array ($ selectedNode , $ comparisonValue , true )
116153 };
117154 }
118155
@@ -121,11 +158,13 @@ public function filter($collection): array
121158 }
122159
123160 if ($ comparisonResult ) {
124- $ return [] = $ value ;
161+ $ return [$ nodeIndex ] = $ node ;
125162 }
126163 }
127164 }
128165
166+ //Keep out returned nodes in the same order they were defined in the original collection
167+ \ksort ($ return );
129168 return $ return ;
130169 }
131170
0 commit comments