Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit afa08c8

Browse files
committedDec 20, 2024··
[JsonEncoder] Add component documentation
1 parent b7f126c commit afa08c8

File tree

1 file changed

+767
-0
lines changed

1 file changed

+767
-0
lines changed
 

‎components/json_encoder.rst

Lines changed: 767 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,767 @@
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

Comments
 (0)
Please sign in to comment.