|
| 1 | +The JsonEncoder Component |
| 2 | +========================= |
| 3 | + |
| 4 | +.. warning:: |
| 5 | + |
| 6 | + This component is :doc:`experimental </contributing/code/experimental>` and |
| 7 | + could be changed at any time without prior notice. |
| 8 | + |
| 9 | +Symfony provides a powerful encoder that is able to encode PHP data |
| 10 | +structures to a JSON stream, and the other way around. That encoder is |
| 11 | +designed to be really efficient, while being able to deal with streams. |
| 12 | + |
| 13 | +The JsonEncoder component should typically be used when working with APIs |
| 14 | +or integrating with third-party APIs. |
| 15 | + |
| 16 | +It can convert an incoming JSON request payload into one or several PHP |
| 17 | +objects that your application can process. |
| 18 | +Then, when sending a response, it can convert the processed PHP objects |
| 19 | +back into a JSON stream for the outgoing response. |
| 20 | + |
| 21 | +Serializer or JsonEncoder? |
| 22 | +-------------------------- |
| 23 | + |
| 24 | +When deciding between using of the :doc:`Serializer component <serializer>` |
| 25 | +or the JsonEncoder component, consider the specific needs of your use case. |
| 26 | + |
| 27 | +The Serializer is ideal for scenarios requiring flexibility, such as |
| 28 | +dynamically manipulating object shapes using normalizers and denormalizers, |
| 29 | +or handling complex objects which multiple serialization representation. |
| 30 | +Plus, it allows working with formats beyond JSON (and even with a custom |
| 31 | +format of yours). |
| 32 | + |
| 33 | +On the other hand, the JsonEncoder component is tailored for simple objects |
| 34 | +and offers significant advantages in terms of performance and memory |
| 35 | +efficiency, especially when working with very large JSON. |
| 36 | +Its ability to stream JSON data makes it particularly valuable for handling |
| 37 | +large datasets or dealing with real-time data processing without loading the |
| 38 | +entire JSON into memory. |
| 39 | + |
| 40 | +There is no silver bullet between the Serializer and the JsonEncoder, the |
| 41 | +choice should be guided by the specific requirements of your use case. |
| 42 | + |
| 43 | +Installation |
| 44 | +------------ |
| 45 | + |
| 46 | +In applications using :ref:`Symfony Flex <symfony-flex>`, run this command to |
| 47 | +install the JsonEncoder component: |
| 48 | + |
| 49 | +.. code-block:: terminal |
| 50 | +
|
| 51 | + $ composer require symfony/json-encoder |
| 52 | +
|
| 53 | +.. include:: /components/require_autoload.rst.inc |
| 54 | + |
| 55 | +Configuration |
| 56 | +------------- |
| 57 | + |
| 58 | +The JsonEncoder achieves speed by generating and storing PHP code. To minimize |
| 59 | +code generation during runtime, you can configure it to pre-generate as much |
| 60 | +PHP code as possible during cache warm up. This can be done by specifying |
| 61 | +where the objects to be encoded or decoded are located: |
| 62 | + |
| 63 | +.. configuration-block:: |
| 64 | + |
| 65 | + .. code-block:: yaml |
| 66 | +
|
| 67 | + # config/packages/json_encoder.yaml |
| 68 | + framework: |
| 69 | + json_encoder: |
| 70 | + paths: # where the objects to be encoded/decoded are located |
| 71 | + App\Encodable\: '%kernel.project_dir%/src/Encodable/*' |
| 72 | +
|
| 73 | + .. code-block:: xml |
| 74 | +
|
| 75 | + <!-- config/packages/json_encoder.xml --> |
| 76 | + <?xml version="1.0" encoding="UTF-8" ?> |
| 77 | + <container xmlns="http://symfony.com/schema/dic/services" |
| 78 | + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| 79 | + xmlns:framework="http://symfony.com/schema/dic/symfony" |
| 80 | + xsi:schemaLocation="http://symfony.com/schema/dic/services |
| 81 | + https://symfony.com/schema/dic/services/services-1.0.xsd |
| 82 | + http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> |
| 83 | +
|
| 84 | + <framework:config> |
| 85 | + <framework:json-encoder> |
| 86 | + <framework:path namespace="App\\Encodable\\">%kernel.project_dir%/src/Encodable/*</framework:path> |
| 87 | + </framework:json-encoder> |
| 88 | + </framework:config> |
| 89 | + </container> |
| 90 | +
|
| 91 | + .. code-block:: php |
| 92 | +
|
| 93 | + // config/packages/json_encoder.php |
| 94 | + use Symfony\Config\FrameworkConfig; |
| 95 | +
|
| 96 | + return static function (FrameworkConfig $framework): void { |
| 97 | + $framework->jsonEncoder() |
| 98 | + ->path('App\\Encodable\\', '%kernel.project_dir%/src/Encodable/*') |
| 99 | + ; |
| 100 | + }; |
| 101 | +
|
| 102 | +Encoding objects |
| 103 | +---------------- |
| 104 | + |
| 105 | +The JsonEncoder works with simple PHP classes that are composed by public |
| 106 | +properties only. For example, let's say that we have the following ``Cat`` |
| 107 | +class:: |
| 108 | + |
| 109 | + // src/Dto/Cat.php |
| 110 | + namespace App\Dto; |
| 111 | + |
| 112 | + class Cat |
| 113 | + { |
| 114 | + public string $name; |
| 115 | + public string $age; |
| 116 | + } |
| 117 | + |
| 118 | +If you want to transform ``Cat`` objects to a JSON string (e.g. to send them |
| 119 | +via an API response), you can get the `json_encoder` service by using the |
| 120 | +:class:`Symfony\\Component\\JsonEncoder\\EncoderInterface` parameter type with |
| 121 | +the ``$jsonEncoder`` name, and use the :method:`Symfony\\Component\\JsonEncoder\\EncoderInterface::encode` |
| 122 | +method: |
| 123 | + |
| 124 | +.. configuration-block:: |
| 125 | + |
| 126 | + .. code-block:: php-symfony |
| 127 | +
|
| 128 | + // src/Controller/CatController.php |
| 129 | + namespace App\Controller; |
| 130 | +
|
| 131 | + use App\Dto\Cat; |
| 132 | + use App\Repository\CatRepository; |
| 133 | + use Symfony\Component\HttpFoundation\Response; |
| 134 | + use Symfony\Component\JsonEncoder\EncoderInterface; |
| 135 | + use Symfony\Component\TypeInfo\Type; |
| 136 | +
|
| 137 | + class CatController |
| 138 | + { |
| 139 | + public function retrieveCats(EncoderInterface $jsonEncoder, CatRepository $catRepository): Response |
| 140 | + { |
| 141 | + $cats = $catRepository->findAll(); |
| 142 | + $type = Type::list(Type::object(Cat::class)); |
| 143 | +
|
| 144 | + $json = $jsonEncoder->encode($cats, $type); |
| 145 | +
|
| 146 | + return new Response($json); |
| 147 | + } |
| 148 | + } |
| 149 | +
|
| 150 | + .. code-block:: php-standalone |
| 151 | +
|
| 152 | + use App\Dto\Cat; |
| 153 | + use App\Repository\CatRepository; |
| 154 | + use Symfony\Component\HttpFoundation\Response; |
| 155 | + use Symfony\Component\JsonEncoder\JsonEncoder; |
| 156 | + use Symfony\Component\TypeInfo\Type; |
| 157 | +
|
| 158 | + // ... |
| 159 | +
|
| 160 | + $jsonEncoder = JsonEncoder::create(); |
| 161 | +
|
| 162 | + $cats = $catRepository->findAll(); |
| 163 | + $type = Type::list(Type::object(Cat::class)); |
| 164 | +
|
| 165 | + $json = $jsonEncoder->encode($cats, $type); |
| 166 | +
|
| 167 | + $response = new Response($json); |
| 168 | +
|
| 169 | + // ... |
| 170 | +
|
| 171 | +Because the :method:`Symfony\\Component\\JsonEncoder\\EncoderInterface::encode` |
| 172 | +method result is either a :phpclass:`Stringable` and a string :phpclass:`Traversable`, |
| 173 | +you can leverage the streaming capabilities of the JsonEncoder, |
| 174 | +by using instead a :class:`Symfony\\Component\\HttpFoundation\\StreamedResponse`: |
| 175 | + |
| 176 | +.. code-block:: diff |
| 177 | +
|
| 178 | + // src/Controller/CatController.php |
| 179 | + namespace App\Controller; |
| 180 | +
|
| 181 | + use App\Dto\Cat; |
| 182 | + use App\Repository\CatRepository; |
| 183 | + use Symfony\Component\HttpFoundation\Response; |
| 184 | + + use Symfony\Component\HttpFoundation\StreamedResponse; |
| 185 | + use Symfony\Component\JsonEncoder\EncoderInterface; |
| 186 | + use Symfony\Component\TypeInfo\Type; |
| 187 | +
|
| 188 | + class CatController |
| 189 | + { |
| 190 | + public function retrieveCats(EncoderInterface $jsonEncoder, CatRepository $catRepository): Response |
| 191 | + { |
| 192 | + $cats = $catRepository->findAll(); |
| 193 | + $type = Type::list(Type::object(Cat::class)); |
| 194 | +
|
| 195 | + $json = $jsonEncoder->encode($cats, $type); |
| 196 | +
|
| 197 | + - return new Response($json); |
| 198 | + + return new StreamedResponse($json); |
| 199 | + } |
| 200 | + } |
| 201 | +
|
| 202 | +
|
| 203 | +Decoding objects |
| 204 | +---------------- |
| 205 | + |
| 206 | +Besides encoding objects to JSON, you can decode JSON to objects. |
| 207 | + |
| 208 | +To do so, you can get the ``json_decoder`` service by using the |
| 209 | +:class:`Symfony\\Component\\JsonEncoder\\DecoderInterface` parameter type |
| 210 | +with the ``$jsonDecoder`` name, and use the :method:`Symfony\\Component\\JsonEncoder\\DecoderInterface::decode` |
| 211 | +method: |
| 212 | + |
| 213 | +.. configuration-block:: |
| 214 | + |
| 215 | + .. code-block:: php-symfony |
| 216 | +
|
| 217 | + // src/Service/TombolaService.php |
| 218 | + namespace App\Service; |
| 219 | +
|
| 220 | + use App\Dto\Cat; |
| 221 | + use Symfony\Component\DependencyInjection\Attribute\Autowire; |
| 222 | + use Symfony\Component\JsonEncoder\DecoderInterface; |
| 223 | + use Symfony\Component\TypeInfo\Type; |
| 224 | +
|
| 225 | + class TombolaService |
| 226 | + { |
| 227 | + private string $catsJsonFile; |
| 228 | +
|
| 229 | + public function __construct( |
| 230 | + private DecoderInterface $jsonDecoder, |
| 231 | + #[Autowire(param: 'kernel.root_dir')] |
| 232 | + string $rootDir, |
| 233 | + ) { |
| 234 | + $this->catsJsonFile = sprintf('%s/var/cats.json', $rootDir); |
| 235 | + } |
| 236 | +
|
| 237 | + public function pickAWinner(): Cat |
| 238 | + { |
| 239 | + $jsonResource = fopen($this->catsJsonFile, 'r'); |
| 240 | + $type = Type::list(Type::object(Cat::class)); |
| 241 | +
|
| 242 | + $cats = $this->jsonDecoder->decode($jsonResource, $type); |
| 243 | +
|
| 244 | + return $cats[rand(0, count($cats) - 1)]; |
| 245 | + } |
| 246 | +
|
| 247 | + /** |
| 248 | + * @return list<string> |
| 249 | + */ |
| 250 | + public function listEligibleCatNames(): array |
| 251 | + { |
| 252 | + $jsonString = file_get_contents($this->catsJsonFile); |
| 253 | + $type = Type::list(Type::object(Cat::class)); |
| 254 | +
|
| 255 | + $cats = $this->jsonDecoder->decode($jsonString, $type); |
| 256 | +
|
| 257 | + return array_column($cats, 'name'); |
| 258 | + } |
| 259 | + } |
| 260 | +
|
| 261 | + .. code-block:: php-standalone |
| 262 | +
|
| 263 | + // src/Service/TombolaService.php |
| 264 | + namespace App\Service; |
| 265 | +
|
| 266 | + use App\Dto\Cat; |
| 267 | + use Symfony\Component\JsonEncoder\DecoderInterface; |
| 268 | + use Symfony\Component\JsonEncoder\JsonDecoder; |
| 269 | + use Symfony\Component\TypeInfo\Type; |
| 270 | +
|
| 271 | + class TombolaService |
| 272 | + { |
| 273 | + private DecoderInterface $jsonDecoder; |
| 274 | + private string $catsJsonFile; |
| 275 | +
|
| 276 | + public function __construct( |
| 277 | + private string $catsJsonFile, |
| 278 | + ) { |
| 279 | + $this->jsonDecoder = JsonDecoder::create(); |
| 280 | + } |
| 281 | +
|
| 282 | + public function pickAWinner(): Cat |
| 283 | + { |
| 284 | + $jsonResource = fopen($this->catsJsonFile, 'r'); |
| 285 | + $type = Type::list(Type::object(Cat::class)); |
| 286 | +
|
| 287 | + $cats = $this->jsonDecoder->decode($jsonResource, $type); |
| 288 | +
|
| 289 | + return $cats[rand(0, count($cats) - 1)]; |
| 290 | + } |
| 291 | +
|
| 292 | + /** |
| 293 | + * @return list<string> |
| 294 | + */ |
| 295 | + public function listEligibleCatNames(): array |
| 296 | + { |
| 297 | + $jsonString = file_get_contents($this->catsJsonFile); |
| 298 | + $type = Type::list(Type::object(Cat::class)); |
| 299 | +
|
| 300 | + $cats = $this->jsonDecoder->decode($jsonString, $type); |
| 301 | +
|
| 302 | + return array_column($cats, 'name'); |
| 303 | + } |
| 304 | + } |
| 305 | +
|
| 306 | +The upper code demonstrates two different approaches to decoding JSON data |
| 307 | +using the JsonEncoder: |
| 308 | + |
| 309 | +* decoding from a stream (``pickAWinner``) |
| 310 | +* decoding from a string (``listEligibleCatNames``). |
| 311 | + |
| 312 | +Both methods work with the same JSON data but differ in memory usage and |
| 313 | +speed optimization. |
| 314 | + |
| 315 | + |
| 316 | +Decoding from a stream |
| 317 | +~~~~~~~~~~~~~~~~~~~~~~ |
| 318 | + |
| 319 | +In the ``pickAWinner`` method, the JSON data is read as a stream using |
| 320 | +:phpfunction:`fopen`. Streams are useful when working with large files |
| 321 | +because the data is processed incrementally rather than loading the entire |
| 322 | +file into memory. |
| 323 | + |
| 324 | +To improve memory efficiency, the JsonEncoder creates _`ghost objects`: https://en.wikipedia.org/wiki/Lazy_loading#Ghost |
| 325 | +instead of fully instantiating objects. Ghosts objects are lightweight |
| 326 | +placeholders that represent the objects but don't fully load their data |
| 327 | +into memory until it's needed. This approach reduces memory usage, especially |
| 328 | +for large datasets. |
| 329 | + |
| 330 | +* Advantage: Efficient memory usage, suitable for very large JSON files. |
| 331 | +* Disadvantage: Slightly slower than decoding a full string because data is loaded on-demand. |
| 332 | + |
| 333 | +Decoding from a string |
| 334 | +~~~~~~~~~~~~~~~~~~~~~~ |
| 335 | + |
| 336 | +In the ``listEligibleCatNames`` method, the entire JSON file is read into |
| 337 | +a string using :phpfunction:`file_get_contents`. This string is then passed |
| 338 | +to the decoder, which fully instantiates all the objects in the JSON data |
| 339 | +upfront. |
| 340 | + |
| 341 | +This approach is faster because all the objects are created immediately, |
| 342 | +making subsequent operations on the data quicker. However, it uses more |
| 343 | +memory since the entire file content and all objects are loaded at once. |
| 344 | + |
| 345 | +* Advantage: Faster processing, suitable for small to medium-sized JSON files. |
| 346 | +* Disadvantage: Higher memory usage, not ideal for large JSON files. |
| 347 | + |
| 348 | +.. tip:: |
| 349 | + |
| 350 | + Prefer stream decoding when working with large JSON files to conserve |
| 351 | + memory. |
| 352 | + |
| 353 | + Prefer string decoding instead when performance is more critical and the |
| 354 | + JSON file size is manageable. |
| 355 | + |
| 356 | +Enabling PHPDoc reading |
| 357 | +----------------------- |
| 358 | + |
| 359 | +The JsonEncoder component can be able to process advanced PHPDoc type |
| 360 | +definitions, such as generics, and read/generate JSON for complex PHP |
| 361 | +objects. |
| 362 | + |
| 363 | +For example, let's consider this ``Shelter`` class that defines a generic |
| 364 | +``TAnimal`` type, which can be a ``Cat`` or a ``Dog``:: |
| 365 | + |
| 366 | + // src/Dto/Shelter.php |
| 367 | + namespace App\Dto; |
| 368 | + |
| 369 | + /** |
| 370 | + * @template TAnimal of Cat|Dog |
| 371 | + */ |
| 372 | + class Shelter |
| 373 | + { |
| 374 | + /** |
| 375 | + * @var list<TAnimal> |
| 376 | + */ |
| 377 | + public array $animals; |
| 378 | + } |
| 379 | + |
| 380 | + |
| 381 | +To enable PHPDoc interpretation, run the following command: |
| 382 | + |
| 383 | +.. code-block:: terminal |
| 384 | +
|
| 385 | + $ composer require phpstan/phpdoc-parser |
| 386 | +
|
| 387 | +Then, when encoding/decoding an instance of the ``Shelter`` class, you can |
| 388 | +specify the concrete type information, and the JsonEncoder will deal with the |
| 389 | +correct JSON structure:: |
| 390 | + |
| 391 | + use App\Dto\Cat; |
| 392 | + use App\Dto\Shelter; |
| 393 | + use Symfony\Component\TypeInfo\Type; |
| 394 | + |
| 395 | + $json = <<<JSON |
| 396 | + { |
| 397 | + "animals": [ |
| 398 | + {"name": "Eva", "age": 29}, |
| 399 | + {...} |
| 400 | + ] |
| 401 | + } |
| 402 | + JSON; |
| 403 | + |
| 404 | + // maps the TAnimal template in Shelter to the Cat concrete type |
| 405 | + $type = Type::generic(Type::object(Shelter::class), Type::object(Cat::class)); |
| 406 | + |
| 407 | + $catShelter = $jsonDecoder->decode($json, $type); // will be populated with cats |
| 408 | + |
| 409 | +Configuring encoding/decoding |
| 410 | +----------------------------- |
| 411 | + |
| 412 | +While it's not recommended to change to object shape and values during |
| 413 | +encoding and decoding, it is sometimes unavoidable. |
| 414 | + |
| 415 | +Configuring the encoded name |
| 416 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 417 | + |
| 418 | +It is possible to configure the JSON key associated to a property thanks to |
| 419 | +the :class:`Symfony\\Component\\JsonEncoder\\Attribute\\EncodedName` |
| 420 | +attribute:: |
| 421 | + |
| 422 | + // src/Dto/Duck.php |
| 423 | + namespace App\Dto; |
| 424 | + |
| 425 | + use Symfony\Component\JsonEncoder\Attribute\EncodedName; |
| 426 | + |
| 427 | + class Duck |
| 428 | + { |
| 429 | + #[EncodedName('@id')] |
| 430 | + public string $id; |
| 431 | + } |
| 432 | + |
| 433 | +By doing so, the ``Duck::$id`` property will be mapped to the ``@id`` JSON key:: |
| 434 | + |
| 435 | + use App\Dto\Duck; |
| 436 | + use Symfony\Component\TypeInfo\Type; |
| 437 | + |
| 438 | + // ... |
| 439 | + |
| 440 | + $duck = new Duck(); |
| 441 | + $duck->id = '/ducks/daffy'; |
| 442 | + |
| 443 | + echo (string) $jsonEncoder->encode($duck, Type::object(Duck::class)); |
| 444 | + |
| 445 | + // This will output: |
| 446 | + // { |
| 447 | + // "@id": "/ducks/daffy" |
| 448 | + // } |
| 449 | + |
| 450 | +Configuring the encoded value |
| 451 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 452 | + |
| 453 | +If you need to manipulate the value related to a property during the encoding |
| 454 | +process, you can use the :class:`Symfony\\Component\\JsonEncoder\\Attribute\\Normalizer` |
| 455 | +attribute. This attribute takes a callable, or a :ref:`normalizer service id <json-encoder-using-normalizer-services>`. |
| 456 | + |
| 457 | +When a callable is specified, it must either be a public static method or |
| 458 | +non-anonymous function with the following signature:: |
| 459 | + |
| 460 | + $normalizer = function (mixed $data, array $options = []): mixed { /* ... */ }; |
| 461 | + |
| 462 | +Then, you just have to specify the function identifier in the attribute:: |
| 463 | + |
| 464 | + // src/Dto/Duck.php |
| 465 | + namespace App\Dto; |
| 466 | + |
| 467 | + use Symfony\Component\JsonEncoder\Attribute\Normalizer; |
| 468 | + |
| 469 | + class Duck |
| 470 | + { |
| 471 | + #[Normalizer('strtoupper')] |
| 472 | + public int $name; |
| 473 | + |
| 474 | + #[Normalizer([self::class, 'formatHeight'])] |
| 475 | + public int $height; |
| 476 | + |
| 477 | + public static function formatHeight(int $denormalized, array $options = []): string |
| 478 | + { |
| 479 | + return sprintf('%.2fcm', $denormalized / 100); |
| 480 | + } |
| 481 | + } |
| 482 | + |
| 483 | +For example, by configuring the ``Duck`` class like above, the ``name`` and |
| 484 | +``height`` values will be normalized during encoding:: |
| 485 | + |
| 486 | + use App\Dto\Duck; |
| 487 | + use Symfony\Component\TypeInfo\Type; |
| 488 | + |
| 489 | + // ... |
| 490 | + |
| 491 | + $duck = new Duck(); |
| 492 | + $duck->name = 'daffy'; |
| 493 | + $duck->height = 5083; |
| 494 | + |
| 495 | + echo (string) $jsonEncoder->encode($duck, Type::object(Duck::class)); |
| 496 | + |
| 497 | + // This will output: |
| 498 | + // { |
| 499 | + // "name": "DAFFY", |
| 500 | + // "height": "50.83cm" |
| 501 | + // } |
| 502 | + |
| 503 | +.. _json-encoder-using-normalizer-services: |
| 504 | + |
| 505 | +Configuring the encoded value with a service |
| 506 | +............................................ |
| 507 | + |
| 508 | +When static methods or functions are not enough, you can normalize the value |
| 509 | +thanks to a normalizer service. |
| 510 | + |
| 511 | +To do so, create a service implementing the :class:`Symfony\\Component\\JsonEncoder\\Encode\\Normalizer\\NormalizerInterface`:: |
| 512 | + |
| 513 | + // src/Encoder/DogUrlNormalizer.php |
| 514 | + namespace App\Encoder; |
| 515 | + |
| 516 | + use Symfony\Component\JsonEncoder\Encode\Normalizer\NormalizerInterface; |
| 517 | + use Symfony\Component\Routing\Generator\UrlGeneratorInterface; |
| 518 | + use Symfony\Component\TypeInfo\Type; |
| 519 | + |
| 520 | + class DogUrlNormalizer implements NormalizerInterface |
| 521 | + { |
| 522 | + public function __construct( |
| 523 | + private UrlGeneratorInterface $urlGenerator, |
| 524 | + ) { |
| 525 | + } |
| 526 | + |
| 527 | + public function normalize(mixed $denormalized, array $options = []): string |
| 528 | + { |
| 529 | + if (!is_int($denormalized)) { |
| 530 | + throw new \InvalidArgumentException(sprintf('The denormalized data must be "int", "%s" given.', get_debug_type($denormalized))); |
| 531 | + } |
| 532 | + |
| 533 | + return $this->urlGenerator->generate('show_dog', ['id' => $denormalized]); |
| 534 | + } |
| 535 | + |
| 536 | + public static function getNormalizedType(): Type |
| 537 | + { |
| 538 | + return Type::string(); |
| 539 | + } |
| 540 | + } |
| 541 | + |
| 542 | +.. note:: |
| 543 | + |
| 544 | + The ``getNormalizedType`` method should return the type of what the value |
| 545 | + will be in the JSON stream. |
| 546 | + |
| 547 | +And then, configure the :class:`Symfony\\Component\\JsonEncoder\\Attribute\\Normalizer` |
| 548 | +attribute to use that service:: |
| 549 | + |
| 550 | + // src/Dto/Dog.php |
| 551 | + namespace App\Dto; |
| 552 | + |
| 553 | + use App\Encoder\DogUrlNormalizer; |
| 554 | + use Symfony\Component\JsonEncoder\Attribute\EncodedName; |
| 555 | + use Symfony\Component\JsonEncoder\Attribute\Normalizer; |
| 556 | + |
| 557 | + class Dog |
| 558 | + { |
| 559 | + #[EncodedName('url')] |
| 560 | + #[Normalizer(DogUrlNormalizer::class)] |
| 561 | + public int $id; |
| 562 | + } |
| 563 | + |
| 564 | +Configuring the decoded value |
| 565 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 566 | + |
| 567 | +You can as well manipulate the value related to a property during decoding |
| 568 | +using the :class:`Symfony\\Component\\JsonEncoder\\Attribute\\Denormalizer` |
| 569 | +attribute, which takes as well a callable, or a :ref:`denormalizer service id <json-encoder-using-denormalizer-services>`. |
| 570 | + |
| 571 | +When a callable is specified, it must either be a public static method or |
| 572 | +non-anonymous function with the following signature:: |
| 573 | + |
| 574 | + $denormalizer = function (mixed $data, array $options = []): mixed { /* ... */ }; |
| 575 | + |
| 576 | +Then, you just have to specify the function identifier in the attribute:: |
| 577 | + |
| 578 | + // src/Dto/Duck.php |
| 579 | + namespace App\Dto; |
| 580 | + |
| 581 | + use Symfony\Component\JsonEncoder\Attribute\Denormalizer; |
| 582 | + |
| 583 | + class Duck |
| 584 | + { |
| 585 | + #[Denormalizer([self::class, 'retrieveFirstName'])] |
| 586 | + public string $firstName; |
| 587 | + |
| 588 | + #[Denormalizer([self::class, 'retrieveLastName'])] |
| 589 | + public string $lastName; |
| 590 | + |
| 591 | + public static function retrieveFirstName(string $normalized, array $options = []): int |
| 592 | + { |
| 593 | + return explode(' ', $normalized)[0]; |
| 594 | + } |
| 595 | + |
| 596 | + public static function retrieveLastName(string $normalized, array $options = []): int |
| 597 | + { |
| 598 | + return explode(' ', $normalized)[1]; |
| 599 | + } |
| 600 | + } |
| 601 | + |
| 602 | +For example, by configuring the ``Duck`` class like above, the ``height`` |
| 603 | +values will be denormalized during decoding:: |
| 604 | + |
| 605 | + use App\Dto\Duck; |
| 606 | + use Symfony\Component\TypeInfo\Type; |
| 607 | + |
| 608 | + // ... |
| 609 | + |
| 610 | + $duck = $jsonDecoder->decode( |
| 611 | + '{"name": "Daffy Duck"}', |
| 612 | + Type::object(Duck::class), |
| 613 | + ); |
| 614 | + |
| 615 | + // The $duck variable will contain: |
| 616 | + // object(Duck)#1 (1) { |
| 617 | + // ["firstName"] => string(5) "Daffy" |
| 618 | + // ["lastName"] => string(4) "Duck" |
| 619 | + // } |
| 620 | + |
| 621 | +.. _json-encoder-using-denormalizer-services: |
| 622 | + |
| 623 | +Configuring the decoded value with a service |
| 624 | +............................................ |
| 625 | + |
| 626 | +When a simple callable is not enough, you can denormalize the value thanks |
| 627 | +to denormalizer services. |
| 628 | + |
| 629 | +To do so, create a service implementing the :class:`Symfony\\Component\\JsonEncoder\\Decode\\Denormalizer\\DenormalizerInterface`:: |
| 630 | + |
| 631 | + // src/Encoder/DuckHeightDenormalizer.php |
| 632 | + namespace App\Encoder; |
| 633 | + |
| 634 | + use Symfony\Component\JsonEncoder\Decode\Denormalizer\DenormalizerInterface; |
| 635 | + use Symfony\Component\TypeInfo\Type; |
| 636 | + |
| 637 | + class DuckHeightDenormalizer implements DenormalizerInterface |
| 638 | + { |
| 639 | + public function __construct( |
| 640 | + private HeightConverter $heightConverter, |
| 641 | + ) { |
| 642 | + } |
| 643 | + |
| 644 | + public function denormalize(mixed $normalized, array $options = []): int |
| 645 | + { |
| 646 | + if (!is_string($denormalized)) { |
| 647 | + throw new \InvalidArgumentException(sprintf('The denormalized data must be "int", "%s" given.', get_debug_type($denormalized))); |
| 648 | + } |
| 649 | + |
| 650 | + $cm = (float) substr($denormalized, 0, -2); |
| 651 | + |
| 652 | + return $this->heightConverter->cmToMm($cm); |
| 653 | + } |
| 654 | + |
| 655 | + public static function getNormalizedType(): Type |
| 656 | + { |
| 657 | + return Type::string(); |
| 658 | + } |
| 659 | + } |
| 660 | + |
| 661 | +.. note:: |
| 662 | + |
| 663 | + The ``getNormalizedType`` method should return the type of what the value |
| 664 | + is in the JSON stream. |
| 665 | + |
| 666 | +And then, configure the :class:`Symfony\\Component\\JsonEncoder\\Attribute\\Denormalizer` |
| 667 | +attribute to use that service:: |
| 668 | + |
| 669 | + // src/Dto/Dog.php |
| 670 | + namespace App\Dto; |
| 671 | + |
| 672 | + use App\Encoder\DuckHeightDenormalizer; |
| 673 | + use Symfony\Component\JsonEncoder\Attribute\Denormalizer; |
| 674 | + |
| 675 | + class Duck |
| 676 | + { |
| 677 | + #[Denormalizer(DuckHeightDenormalizer::class)] |
| 678 | + public int $height; |
| 679 | + } |
| 680 | + |
| 681 | +.. tip:: |
| 682 | + |
| 683 | + The normalizers and denormalizers will be intesively called during the |
| 684 | + encoding. So be sure to keep them as fast as possible (avoid calling |
| 685 | + external APIs or the database for example). |
| 686 | + |
| 687 | +Configure keys and values dynamically |
| 688 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 689 | + |
| 690 | +The JsonEncoder leverages services implementing the :class:`Symfony\\Component\\JsonEncoder\\Mapping\\PropertyMetadataLoaderInterface` |
| 691 | +to determine the shape and values of objects during encoding. |
| 692 | + |
| 693 | +These services are highly flexible and can be decorated to handle dynamic |
| 694 | +configurations, offering much greater power compared to using attributes:: |
| 695 | + |
| 696 | + // src/Encoder/SensitivePropertyMetadataLoader.php |
| 697 | + namespace App\Encoder\SensitivePropertyMetadataLoader; |
| 698 | + |
| 699 | + use App\Dto\SensitiveInterface; |
| 700 | + use App\Encode\EncryptorNormalizer; |
| 701 | + use Symfony\Component\JsonEncoder\Mapping\PropertyMetadata; |
| 702 | + use Symfony\Component\JsonEncoder\Mapping\PropertyMetadataLoaderInterface; |
| 703 | + use Symfony\Component\TypeInfo\Type; |
| 704 | + |
| 705 | + class SensitivePropertyMetadataLoader implements PropertyMetadataLoaderInterface |
| 706 | + { |
| 707 | + public function __construct( |
| 708 | + private PropertyMetadataLoaderInterface $decorated, |
| 709 | + ) { |
| 710 | + } |
| 711 | + |
| 712 | + public function load(string $className, array $options = [], array $context = []): array |
| 713 | + { |
| 714 | + $propertyMetadataMap = $this->decorated->load($className, $config, $context); |
| 715 | + |
| 716 | + if (!is_a($className, SensitiveInterface::class, true)) { |
| 717 | + return $propertyMetadataMap; |
| 718 | + } |
| 719 | + |
| 720 | + // you can configure normalizers/denormalizers |
| 721 | + foreach ($propertyMetadataMap as $jsonKey => $metadata) { |
| 722 | + if (!in_array($metadata->getName(), $className::getPropertiesToEncrypt(), true)) { |
| 723 | + continue; |
| 724 | + } |
| 725 | + |
| 726 | + $propertyMetadataMap[$jsonKey] = $metadata |
| 727 | + ->withType(Type::string()) |
| 728 | + ->withAdditionalNormalizer(EncryptorNormalizer::class); |
| 729 | + } |
| 730 | + |
| 731 | + // you can remove existing properties |
| 732 | + foreach ($propertyMetadataMap as $jsonKey => $metadata) { |
| 733 | + if (!in_array($metadata->getName(), $className::getPropertiesToRemove(), true)) { |
| 734 | + continue; |
| 735 | + } |
| 736 | + |
| 737 | + unset($propertyMetadataMap[$jsonKey]); |
| 738 | + } |
| 739 | + |
| 740 | + // you can rename JSON keys |
| 741 | + foreach ($propertyMetadataMap as $jsonKey => $metadata) { |
| 742 | + $propertyMetadataMap[md5($jsonKey)] = $propertyMetadataMap[$jsonKey]; |
| 743 | + unset($propertyMetadataMap[$jsonKey]); |
| 744 | + } |
| 745 | + |
| 746 | + // you can add virtual properties |
| 747 | + $propertyMetadataMap['is_sensitive'] = new PropertyMetadata( |
| 748 | + name: 'theNameWontBeUsed', |
| 749 | + type: Type::bool(), |
| 750 | + normalizers: [self::trueValue(...)], |
| 751 | + ); |
| 752 | + |
| 753 | + return $propertyMetadataMap; |
| 754 | + } |
| 755 | + |
| 756 | + public static function trueValue(): bool |
| 757 | + { |
| 758 | + return true; |
| 759 | + } |
| 760 | + } |
| 761 | + |
| 762 | +However, this flexibility comes with complexity. Decorating property metadata |
| 763 | +loaders requires a deep understanding of the system. |
| 764 | + |
| 765 | +For most use cases, the attributes approach is sufficient, and the dynamic |
| 766 | +capabilities of property metadata loaders should be reserved for scenarios |
| 767 | +where their additional power is genuinely necessary. |
0 commit comments