-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[JsonPath] Add FrameworkBundle integration #60083
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.3
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\DependencyInjection\Loader\Configurator; | ||
|
||
use Symfony\Component\JsonPath\JsonCrawler; | ||
use Symfony\Component\JsonPath\JsonCrawlerInterface; | ||
|
||
return static function (ContainerConfigurator $container) { | ||
$container->services() | ||
->set('json_path.crawler', JsonCrawler::class) | ||
->alias(JsonCrawlerInterface::class, 'json_path.crawler'); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\JsonPath; | ||
|
||
use Symfony\Component\JsonPath\Exception\InvalidArgumentException; | ||
use Symfony\Component\JsonPath\Exception\JsonCrawlerException; | ||
|
||
/** | ||
* @author Alexandre Daubois <alex.daubois@gmail.com> | ||
* | ||
* @experimental | ||
*/ | ||
interface CrawlerInterface | ||
{ | ||
/** | ||
* @return list<array|string|float|int|bool|null> | ||
* | ||
* @throws InvalidArgumentException When the data source provided to the crawler cannot be decoded | ||
* @throws JsonCrawlerException When a syntax error occurs in the provided JSON path | ||
*/ | ||
public function find(string|JsonPath $query): array; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,7 +28,7 @@ | |
* | ||
* @experimental | ||
*/ | ||
final class JsonCrawler implements JsonCrawlerInterface | ||
final class JsonCrawler implements CrawlerInterface, JsonCrawlerInterface | ||
{ | ||
private const RFC9535_FUNCTIONS = [ | ||
'length' => true, | ||
|
@@ -39,16 +39,23 @@ final class JsonCrawler implements JsonCrawlerInterface | |
]; | ||
|
||
/** | ||
* @param resource|string $raw | ||
* @param resource|string|array $data | ||
*/ | ||
public function __construct( | ||
private readonly mixed $raw, | ||
) { | ||
if (!\is_string($raw) && !\is_resource($raw)) { | ||
throw new InvalidArgumentException(\sprintf('Expected string or resource, got "%s".', get_debug_type($raw))); | ||
public function __construct(private mixed $data = []) | ||
{ | ||
if (!\is_string($data) && !\is_resource($data) && !\is_array($data)) { | ||
throw new \TypeError(\sprintf('Argument #1 ($data) must be of type string, array or resource, %s given.', get_debug_type($data))); | ||
} | ||
} | ||
|
||
/** | ||
* @param resource|string|array $data | ||
*/ | ||
public function fromJson(mixed $data): CrawlerInterface | ||
alexandre-daubois marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about making the method static? And I would question if we really need an interface for it? And if so, if it's name wouldn't better be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Making the method static wouldn't allow us to plug properly with DI and allow further steps like this one. The "factory" name is not used per this comment and this one. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AFAIK, we can have static factories in the DI. But my main question here is about the aim of that method. In fact, such code will look weird to me: $foo = (new JsonCrawler('"something"'))
->fromJson('"otherThing"'); Instead, static method looks more logical to me (and still allows you to use the builder pattern): $foo = JsonCrawler::fromJson('"otherThing"')
>withCustomFunctions(...); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In your last snippet, you call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You might be able to combine static factories and service configurators maybe? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hell no, not configurators. But all this makes me think we're thinking out of the blue. Let's not do anything until we actually do have such transformers. If we have some lessons to take from this PR to prepare the next steps, let's keep that, but let's remove the autowiring/DI part to the moment where we'll really need it. |
||
{ | ||
return new self($data); | ||
} | ||
|
||
public function find(string|JsonPath $query): array | ||
{ | ||
return $this->evaluate(\is_string($query) ? new JsonPath($query) : $query); | ||
|
@@ -58,29 +65,33 @@ private function evaluate(JsonPath $query): array | |
{ | ||
try { | ||
$tokens = JsonPathTokenizer::tokenize($query); | ||
$json = $this->raw; | ||
$json = $this->data; | ||
|
||
if (\is_resource($this->raw)) { | ||
if (\is_resource($this->data)) { | ||
if (!class_exists(Splitter::class)) { | ||
throw new \LogicException('The JsonStreamer package is required to evaluate a path against a resource. Try running "composer require symfony/json-streamer".'); | ||
} | ||
|
||
$simplified = JsonPathUtils::findSmallestDeserializableStringAndPath( | ||
$tokens, | ||
$this->raw, | ||
$this->data, | ||
); | ||
|
||
$tokens = $simplified['tokens']; | ||
$json = $simplified['json']; | ||
} | ||
|
||
try { | ||
$data = json_decode($json, true, 512, \JSON_THROW_ON_ERROR); | ||
} catch (\JsonException $e) { | ||
throw new InvalidJsonStringInputException($e->getMessage(), $e); | ||
} | ||
if (\is_array($json)) { | ||
$current = [$json]; | ||
} else { | ||
try { | ||
$data = json_decode($json, true, 512, \JSON_THROW_ON_ERROR); | ||
} catch (\JsonException $e) { | ||
throw new InvalidJsonStringInputException($e->getMessage(), $e); | ||
} | ||
|
||
$current = [$data]; | ||
$current = [$data]; | ||
} | ||
|
||
foreach ($tokens as $token) { | ||
$next = []; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,9 +11,6 @@ | |
|
||
namespace Symfony\Component\JsonPath; | ||
|
||
use Symfony\Component\JsonPath\Exception\InvalidArgumentException; | ||
use Symfony\Component\JsonPath\Exception\JsonCrawlerException; | ||
|
||
/** | ||
* @author Alexandre Daubois <alex.daubois@gmail.com> | ||
* | ||
|
@@ -22,10 +19,7 @@ | |
interface JsonCrawlerInterface | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we still need this interface? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this is our factory and the interface used to be used in service injection. One of the next step is adding something like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder though if that doesn't mean that we should rather split the crawler and it's factory. Currently both concerned are mixed in one class which also means that the crawler needs to allow being constructed without data while that shouldn't be needed if the factory was a different class, right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed. We discussed a bit about it with @nicolas-grekas. We agreed that we're fine having everything in one class as well as having a no-op crawler when created without data (i.e. returning an empty array for all paths). Is there a limitation that you see with this implementation maybe? I went for the separate factory class at first, but I actually like the fact that if you need a crawler in your service, you can inject There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Having the 2 interfaces implemented by the same class is not a problem when using interfaces for typing. I would prefer There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer JsonCrawlerInterface - it feels less convoluted to me, and is explicit about the intended use case - querying json documents. I also prefer one class to implement the two interfaces. Optimizing the constructor doesn't look worth the split to me. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My main concern with mixing responsibilities in one class is that you could have a crawler in an intermediate state while building and then accidentally pass around the wrong crawler instance which would then behave differently than what you did expect. By separating the crawler and its factory (or probably rather a builder) PHP's type system would save you from such a mistake. |
||
{ | ||
/** | ||
* @return list<array|string|float|int|bool|null> | ||
* | ||
* @throws InvalidArgumentException When the JSON string provided to the crawler cannot be decoded | ||
* @throws JsonCrawlerException When a syntax error occurs in the provided JSON path | ||
* @param resource|string|array $data | ||
*/ | ||
public function find(string|JsonPath $query): array; | ||
public function fromJson(mixed $data): CrawlerInterface; | ||
} |
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.
Maybe we can add a functional test to make sure that the services are properly registered and usable? (You can find such tests in
Symfony\Bundle\FrameworkBundle\Tests\Functional