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
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
// src/Transformer/MoneyValueObjectTransformer.php
namespace App\Transformer;
use App\ValueObject\Money;
use Symfony\Component\JsonStreamer\Transformer\ValueObjectTransformerInterface;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\BuiltinType;
/**
* @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:
1 2 3 4 5
// 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
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:
1 2 3 4 5
use Symfony\Component\TypeInfo\Type;
$json = $jsonStreamWriter->write($task, Type::object(Task::class), [
'date_interval_format' => 'P%yY%mM%dDT%hH%iM%sS',
]);
Configuring the DateTime Timezone
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:
1 2 3 4 5 6 7 8 9
use Symfony\Component\TypeInfo\Type;
$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
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:
1 2 3 4 5 6
# 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():
1 2 3 4 5
# config/packages/framework.yaml
framework:
json_streamer:
default_options:
my_custom_option: 'my_custom_value'
Custom JsonPath Functions
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:
1 2 3 4 5 6 7 8 9 10
use Symfony\Component\JsonPath\Attribute\AsJsonPathFunction;
#[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:
1 2 3
$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)].