-
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
[FrameworkBundle][JsonPath] Add support for custom JsonPath functions #61517
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
base: 7.4
Are you sure you want to change the base?
[FrameworkBundle][JsonPath] Add support for custom JsonPath functions #61517
Conversation
b5a68b4
to
42a26ef
Compare
42a26ef
to
1927fa4
Compare
abstract class AbstractJsonPathFunction implements JsonPathFunctionInterface | ||
{ | ||
public function __construct( | ||
private ?string $name = null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should not use a promoted property, to decouple the parameter type (nullable) from the private property type (not nullable)
*/ | ||
trait JsonPathFunctionArgumentTrait | ||
{ | ||
public static function assertArgumentsCount(array $args, int $expectedCount): void |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why making the public static methods if this is in a trait ?
Either we should have public static utilities in a dedicated class expected to be called statically, or we should have a trait defining private instance methods (so that using the trait does not expend the public API of the class implementing a function)
interface JsonPathFunctionInterface | ||
{ | ||
/** | ||
* @param array<string> $args Array of argument strings as they appear in the expression |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would even be list<string>
to be more precise, as they are always provided as a list.
* | ||
* @param array<string> $args Array of argument strings as they appear in the expression | ||
* | ||
* @throws \InvalidArgumentException When arguments are invalid |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't this use an exception class of the component instead ?
public function __construct(iterable $functions = []) | ||
{ | ||
foreach ($functions as $function) { | ||
$this->addFunction($function); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of injecting an iterable that is used in the constructor (breaking any lazy-loading of the iterator argument provided by the DI component), I suggest injecting a service locator instead (which would require knowing the name statically, which could be done by always specifying the name in the attribute instead of the getName
method in the instance)
* @author Alexandre Daubois <alex.daubois@gmail.com> | ||
*/ | ||
#[AsJsonPathFunction(name: 'count')] | ||
final class CountFunction extends AbstractJsonPathFunction implements JsonPathComparableFunctionInterface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should these core functions be tagged as @internal
as the JsonCrawler is registering them on its own and so there is no reason for ever accessing them directly outside the component ?
*/ | ||
trait JsonPathFunctionArgumentTrait | ||
{ | ||
public static function assertArgumentsCount(array $args, int $expectedCount): void |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type of $args
must be specified in phpdoc
@@ -28,7 +27,7 @@ final class JsonPathTokenizer | |||
/** | |||
* @return JsonPathToken[] | |||
*/ | |||
public static function tokenize(JsonPath $query): array | |||
public static function tokenize(JsonPath $query, ?callable $functionValidator = null): array |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest documenting the signature of that callable. It will make it easier to contributors reading that code in the future (same in other methods where it is forwarded).
try { | ||
$functionValidator($functionName, $args); | ||
} catch (\InvalidArgumentException $e) { | ||
throw new InvalidJsonPathException($e->getMessage(), $position); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$e
should be attached as previous exception.
} | ||
|
||
$nodelistSize = \count($results); | ||
if (1 < $nodelistSize) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we could remove this check, using only return 1 === $nodelistSize ? $results[0] : null;
for the same result.
Requires #61132. Only second commit (1927fa4) should be reviewed.
This PR allows to create custom JsonPath functions thanks to the
#[AsJsonPathFunction]
attribute:Then, it's used like this:
All RFC built-in functions are converted to use the new
AbstractJsonPathFunction
to avoid special case handling in the crawler code. This uses the approach detailed here: #60624 (comment). What's also nice is that built-in functions now have their very own unit tests as well.