Home / Symfony / New in Symfony 8.1: Improved JSON Streaming and Querying

New in Symfony 8.1: Improved JSON Streaming and Querying

Symfony includes two components dedicated to working with JSON:

  • JsonStreamer encodes PHP data into JSON and decodes JSON back into PHP
    objects by streaming the contents, which provides high performance and low
    memory usage even for large payloads;
  • JsonPath queries and extracts values from JSON documents using the standard
    JSONPath syntax.

Symfony 8.1 improves both components in several ways.

Handling Value Objects


Mathias Arlaud
Contributed by
Mathias Arlaud
in
#63339

By default, JsonStreamer serializes an object by traversing its properties one
by one. However, some objects are better represented as a single scalar value
than as a nested JSON object. Think of a Money object encoded as "100 EUR"
or a temperature represented as a number.

Symfony 8.1 introduces a generic mechanism for this called value objects.
Implement the new ValueObjectTransformerInterface to define how an object
maps to and from a scalar value:

// src/Transformer/MoneyValueObjectTransformer.php
namespace AppTransformer;

use AppValueObjectMoney;
use SymfonyComponentJsonStreamerTransformerValueObjectTransformerInterface;
use SymfonyComponentTypeInfoType;
use SymfonyComponentTypeInfoTypeBuiltinType;

/**
 * @implements ValueObjectTransformerInterface<Money, string>
 */
class MoneyValueObjectTransformer implements ValueObjectTransformerInterface
{
    public function transform(object $object, array $options = []): int|float|string|bool|null
    {
        return $object->amount.' '.$object->currency;
    }

    public function reverseTransform(int|float|string|bool|null $scalar, array $options = []): object
    {
        [$amount, $currency] = explode(' ', $scalar);

        return new Money((int) $amount, $currency);
    }

    public static function getStreamValueType(): BuiltinType
    {
        return Type::string();
    }

    public static function getValueObjectClassName(): string
    {
        return Money::class;
    }
}

Symfony auto-registers these transformers, and the generated code then calls
the transformer instead of traversing the object’s properties:

// write: "100 EUR"
$json = $jsonStreamWriter->write(new Money(100, 'EUR'), Type::object(Money::class));

// read: "100 EUR" -> Money(100, 'EUR')
$money = $jsonStreamReader->read('"100 EUR"', Type::object(Money::class));

Built-in DateInterval and DateTimeZone Value Objects


Mathias Arlaud
Contributed by
Mathias Arlaud
in
#63742
and #63879

Building on the value object mechanism, Symfony 8.1 now handles DateInterval
and DateTimeZone objects as value objects out of the box, following the same
approach already used for DateTimeInterface.

DateInterval objects are serialized to ISO 8601 duration strings (e.g.
P2Y6M1DT12H30M5S), and DateTimeZone objects use their name (e.g.
"Europe/Paris" or "+02:00"). You can customize the duration format with
the date_interval_format option:

use SymfonyComponentTypeInfoType;

$json = $jsonStreamWriter->write($task, Type::object(Task::class), [
    'date_interval_format' => 'P%yY%mM%dDT%hH%iM%sS',
]);

Configuring the DateTime Timezone


Mathias Arlaud
Contributed by
Mathias Arlaud
in
#63735

When encoding or decoding DateTimeInterface objects, you can now convert the
timezone by passing the date_time_timezone option as a string or a
DateTimeZone instance:

use SymfonyComponentTypeInfoType;

$json = $jsonStreamWriter->write($event, Type::object(Event::class), [
    'date_time_timezone' => 'Asia/Tokyo',
]);

$event = $jsonStreamReader->read($json, Type::object(Event::class), [
    'date_time_timezone' => new DateTimeZone('America/Mexico_City'),
]);

Defining Default Options


Mathias Arlaud
Contributed by
Mathias Arlaud
in
#62599

Repeating the same options on every write() and read() call is tedious.
Symfony 8.1 lets you define default options for the entire application, so they
are applied automatically to every operation:

# config/packages/framework.yaml
framework:
    json_streamer:
        default_options:
            # encode properties whose value is null
            include_null_properties: true

You can also define your own custom options here. Symfony passes them to your
transformers, so you can read them inside transform() and reverseTransform():

# config/packages/framework.yaml
framework:
    json_streamer:
        default_options:
            my_custom_option: 'my_custom_value'

Custom JsonPath Functions


Alexandre Daubois
Contributed by
Alexandre Daubois
in
#62823

The JsonPath component already supports the standard functions defined by
RFC 9535 (length(), count(), match(), etc.). Symfony 8.1 lets
you register your own functions and use them inside filter expressions.

Create an invokable class and apply the #[AsJsonPathFunction] attribute to
it. Symfony registers the function automatically and makes it available in all
JsonPath queries:

use SymfonyComponentJsonPathAttributeAsJsonPathFunction;

#[AsJsonPathFunction('upper')]
final class UppercaseFunction
{
    public function __invoke(mixed $value): ?string
    {
        return is_string($value) ? strtoupper($value) : null;
    }
}

Inject the JsonPathCrawlerInterface to get a crawler pre-configured with
all your custom functions, then use them in any expression:

$crawler = $crawlerFactory->crawl($json);

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

The number of arguments accepted by the function is inferred automatically from
the __invoke() method signature. The optional returnType argument
controls where the function can be used: a FunctionReturnType::Value function
(the default) works in comparisons like the example above, while a
FunctionReturnType::Logical function can be used as a standalone filter test
such as $.items[?is_positive(@.value)].


Sponsor the Symfony project.
Tagged:

Leave a Reply

Your email address will not be published. Required fields are marked *