Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

alexandre-daubois
Copy link
Member

@alexandre-daubois alexandre-daubois commented Aug 25, 2025

Q A
Branch? 7.4
Bug fix? no
New feature? yes
Deprecations? no
Issues -
License MIT

Requires #61132. Only second commit (1927fa4) should be reviewed.

This PR allows to create custom JsonPath functions thanks to the #[AsJsonPathFunction] attribute:

<?php

namespace App\JsonPath;

use Symfony\Component\JsonPath\Functions\AbstractJsonPathFunction;
use Symfony\Component\JsonPath\Functions\AsJsonPathFunction;
use Symfony\Component\JsonPath\Functions\JsonPathFunctionArgumentTrait;

#[AsJsonPathFunction(name: 'upper')]
final class UpperFunction extends AbstractJsonPathFunction
{
    use JsonPathFunctionArgumentTrait;

    public function __invoke(array $args, mixed $context): ?string
    {
        $results = $args[0] ?? [];
        $value = \is_array($results) ? ($results[0] ?? null) : $results;

        return \is_string($value) ? strtoupper($value) : null;
    }

    public function validate(array $args): void
    {
        self::assertArgumentsCount($args, 1);
    }
}

Then, it's used like this:

<?php

namespace App\Command;

// ...

#[AsCommand(name: 'cmd', description: 'Hello PhpStorm')]
class MyCommand extends Command
{
    public function __construct(
        private JsonPathFunctionsProviderInterface $jsonPathFunctionsProvider,
    ) {
        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $crawler = new JsonCrawler(<<<JSON
            {"name": "test", "items": [{"title": "hello"}, {"title": "world"}]}
        JSON, $this->jsonPathFunctionsProvider); // <-- provide the functions provider

        $result = $crawler->find('$.items[?upper(@.title) == "HELLO"]');

        dump($result);

        return Command::SUCCESS;
    }
}

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.

@alexandre-daubois alexandre-daubois force-pushed the jp-functions-provider branch 2 times, most recently from b5b5819 to 3147335 Compare August 28, 2025 10:16
@alexandre-daubois
Copy link
Member Author

Thanks, I addressed all your comments. I introduced a locator approach which should address your concerns in b5b5819

@snoob
Copy link
Contributor

snoob commented Sep 1, 2025

question: Instead of introducting a new way to define function can't we use the expression language as has already been done for the components routing and security ?

@alexandre-daubois
Copy link
Member Author

This was actually my first idea. However, the RFC defines that you can add custom functions, not necessarily custom expressions. The way it's done currently allows to easily plug on the existing logic. Introducing expression language would bring many challenges (for example, binding JSON data and the path to the expression in some way). I think it's a better choice to not add the dependency and keep the component as standalone as possible (and it's also easier I think). However, nothing would stop one from defining an expr() function and implement the logic 🙂

Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

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

Partial review, I only now realize #61132 should be merged first 😬

@alexandre-daubois alexandre-daubois force-pushed the jp-functions-provider branch 3 times, most recently from 323351e to 27495a8 Compare September 12, 2025 14:14
@alexandre-daubois alexandre-daubois force-pushed the jp-functions-provider branch 2 times, most recently from b66d9a0 to cbf332a Compare September 12, 2025 14:23
@@ -26,9 +25,11 @@ final class JsonPathTokenizer
private const BARE_LITERAL_REGEX = '(true|false|null|\d+(\.\d+)?([eE][+-]?\d+)?|\'[^\']*\'|"[^"]*")';

/**
* @param callable(string $functionName, list<string> $args): void|null $functionValidator
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* @param callable(string $functionName, list<string> $args): void|null $functionValidator
* @param callable(string $functionName, list<string> $args): void $functionValidator

voisd|null does not make sense

Copy link
Member Author

Choose a reason for hiding this comment

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

It's actually that the param is either a callable returning void, either null. I agree it looks ambiguous, but if I'm correct, CS fixer forces this order.

@alexandre-daubois
Copy link
Member Author

To match the RFC and implement custom functions, Nothing must actually be exposed to the user (compared to 7.3 where it's hidden). I'll take care of your comments in the next few days.

@stof
Copy link
Member

stof commented Sep 12, 2025

If we need to expose Nothing as a non-internal API, I suggest reverting the change that replaced the single-case enum with an stdClass object stored in a private property that could be used as a marker object.
the single-case enum will provide a typesafe clean API for that use case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants