Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

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.

src/Symfony/Component/JsonPath/Functions/CountFunction.php Outdated Show resolved Hide resolved
src/Symfony/Component/JsonPath/Functions/ValueFunction.php Outdated Show resolved Hide resolved
@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 😬

src/Symfony/Bundle/FrameworkBundle/composer.json Outdated Show resolved Hide resolved
src/Symfony/Component/JsonPath/JsonPathUtils.php Outdated Show resolved Hide resolved
src/Symfony/Component/JsonPath/JsonPathUtils.php Outdated Show resolved Hide resolved
@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
src/Symfony/Component/JsonPath/Functions/MatchFunction.php Outdated Show resolved Hide resolved
src/Symfony/Component/JsonPath/Functions/MatchFunction.php Outdated Show resolved Hide resolved
src/Symfony/Component/JsonPath/CHANGELOG.md Outdated Show resolved Hide resolved
src/Symfony/Component/JsonPath/JsonCrawler.php Outdated Show resolved Hide resolved
@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.

nicolas-grekas added a commit that referenced this pull request Sep 15, 2025
… value (alexandre-daubois)

This PR was merged into the 7.3 branch.

Discussion
----------

[JsonPath] Add `Nothing` enum to support special nothing value

| Q             | A
| ------------- | ---
| Branch?       | 7.3
| Bug fix?      | no
| New feature?  | no
| Deprecations? | no
| Issues        | -
| License       | MIT

#61517 introduces custom functions mechanism. For consistency, RFC built-in functions will also use the new functions mechanism. In order to fully work, functions must be able to return the special `nothing` value defined by the RFC. This PR exposes the Nothing enum to the public API instead of storing it internally in the JsonCrawler with stdClass.

Context: #61517 (comment)

Commits
-------

dc4d528 [JsonPath] Add `Nothing` enum to support special nothing value
src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md Outdated Show resolved Hide resolved
$value = \is_array($valueResults) ? ($valueResults[0] ?? null) : $valueResults;
$pattern = \is_array($patternResults) ? ($patternResults[0] ?? null) : $patternResults;

return \is_string($value) && \is_string($pattern) && @preg_match(\sprintf('/^%s$/u', JsonPathUtils::normalizeRegex($pattern)), $value);
Copy link
Member

Choose a reason for hiding this comment

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

is there any recommended behavior when the regexp is invalid? should we decide to throw?

Copy link
Member Author

@alexandre-daubois alexandre-daubois Sep 23, 2025

Choose a reason for hiding this comment

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

Here's what the RFC states for both match and search:

If the first argument is not a string or the second argument is not a string conforming to [RFC9485], the result is LogicalFalse

What to do if the regex is invalid is not explicitly explained. I would say that the current implementation is good enough and also simpler.

@alexandre-daubois alexandre-daubois force-pushed the jp-functions-provider branch 2 times, most recently from f0ec479 to d444902 Compare September 23, 2025 13:13
@alexandre-daubois alexandre-daubois added the ❄️ Feature Freeze Important Pull Requests to finish before the next Symfony "feature freeze" label Oct 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feature FrameworkBundle JsonPath ❄️ Feature Freeze Important Pull Requests to finish before the next Symfony "feature freeze" Status: Needs Review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.