8000 feature #51718 [Serializer] [JsonEncoder] Introducing the component (… · symfony/symfony@c198130 · GitHub
[go: up one dir, main page]

Skip to content

Commit c198130

Browse files
committed
feature #51718 [Serializer] [JsonEncoder] Introducing the component (mtarld)
This PR was merged into the 7.3 branch. Discussion ---------- [Serializer] [JsonEncoder] Introducing the component | Q | A | ------------- | --- | Branch? | 7.2 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | | License | MIT | Doc PR | TODO This PR introduces new component: `JsonEncoder` > [!NOTE] > This is the continuation of a Serializer revamp trial, the previous PR description is available [here](https://gist.github.com/mtarld/2c6e00eb2f01e4726ae16ca6528a998b). ## Why? The `Serializer` component is a library designed to normalize PHP data structures in raw associative arrays, and then encode them in a large variety of formats, which offers a high degree of flexibility. However, that flexibility has some drawbacks: - Data shapes get computed every time (de)serialization happens, which is very expensive as it implies resource-intensive calls such as reflection. - Each time the serializer is called, normalizers and encoders are tried until one supporting the given data is found. While this pattern works well when looping through a relatively small amount of services (e.g. security authenticators), it rapidly becomes costly as the number of normalizers/encoders grows, even though the situation has been slightly improved in 6.3 with the addition of `getSupportedTypes()`. - The whole normalized data is at one point stored in memory, this can cause memory issues when dealing with huge collections. Plus, that degree of flexibility isn't that often needed. Indeed, there are many use cases where we use the Serializer component to serialize data without intensive modification (IE: without custom normalization). And in these cases, the flexibility degrades a lot of performances for nothing. That's why this PR introduces the `JsonEncoder` component, which is focused on performance to address the above use case for the specific JSON format. The DNA of that component is to be a fast and modern JSON parser and encoder. It fixes many issues of the native `json_encode` and `json_decode` PHP functions: streaming, on-demand parsing, generics handling, ability to create strongly typed objects instead of raw associative arrays in one pass, etc. We can see the difference between the Serializer component and the JsonEncoder component like the difference between Doctrine ORM and Doctrine DBAL. Indeed, the DBAL can be considered as a sub-layer of ORM, and when precise and performance-related stuff is needed, developers will skip the ORM layer to deal with the DBAL one directly. And it's the very same difference between the Serializer and the JsonEncoder, when precise and performance-related stuff is needed, developers will skip the normalization layer, by fine-tuning the data mapping in their userland and deal with the encoding layer directly. Therefore, the JsonEncoder can (and probably should) at some point replace the existing JSON encoder of the Serializer, so that only one JSON encoder exists in the Symfony ecosystem. ## API Contrary to the `Symfony\Component\Serializer\SerializerInterface`, which has two methods `serialize` and `deserialize`, the new design will instead introduce four new interfaces. These compose the main part of the available API. ```php <?php namespace Symfony\Component\Encoder; use Symfony\Component\TypeInfo\Type; /** * `@template` T of array<string, mixed> */ interface EncoderInterface { /** * `@param` T $options * * `@return` \Traversable<string>&\Stringable */ public function encode(mixed $data, Type $type, array $options = []): \Traversable&\Stringable; } ``` ```php <?php namespace Symfony\Component\JsonEncoder; use Symfony\Component\JsonEncoder\Stream\StreamReaderInterface; use Symfony\Component\TypeInfo\Type; /** * `@template` T of array<string, mixed> */ interface DecoderInterface { /** * `@param` resource|string $input * `@param` T $options */ public function decode($input, Type $type, array $options = []): mixed; } ``` As you can notice, there is no `$format` parameter. It is indeed logical because an encoder knows how to deal with only one format. ## Usage example Install the component ```bash composer require symfony/json-encoder ``` Configure PHP attributes: ```php <?php use Symfony\Component\JsonEncoder\Attribute\Denormalizer; use Symfony\Component\JsonEncoder\Attribute\EncodedName; use Symfony\Component\JsonEncoder\Attribute\Normalizer; class Dummy { #[EncodedName('`@id`')] public int $id; public string $name; #[Normalizer(DoubleIntAndCastToStringNormalizer::class)] #[Denormalizer(DivideStringAndCastToIntDenormalizer::class)] public int $price; } ``` Add the proper normalizers/denormalizers: ```php <?php use Symfony\Component\JsonEncoder\Encode\Normalizer\NormalizerInterface; use Symfony\Component\TypeInfo\Type; final class DoubleIntAndCastToStringNormalizer implements NormalizerInterface { public function normalize(mixed $data, array $options = []): mixed { return (string) (2 * $config['scale'] * $data); } public static function getNormalizedType(): Type { return Type::string(); } } // --- use Symfony\Component\JsonEncoder\Decode\Denormalizer\DenormalizerInterface; use Symfony\Component\TypeInfo\Type; final class DivideStringAndCastToIntDenormalizer implements DenormalizerInterface { public function denormalize(mixed $data, array $options = []): mixed { return (int) (((int) $data) / (2 * $config['scale'])); } public static function getNormalizedType(): Type { return Type::string(); } } ``` Then use the encoder/decoder: ```php <?php use Symfony\Component\JsonEncoder\JsonEncoder; use Symfony\Component\JsonEncoder\JsonDecoder; use Symfony\Component\JsonEncoder\Stream\MemoryStream; use Symfony\Component\TypeInfo\Type; final class MyService { public function __invoke(): void { $jsonEncoder = JsonEncoder::create(); $jsonDecoder = JsonDecoder::create(); // encode dummy to JSON string $encoded = (string) $jsonEncoder->encode(new Dummy(), Type::object(Dummy::class)); // encode dummy to JSON iterable string $encoded = ''; foreach ($jsonEncoder->encode(new Dummy(), Type::object(Dummy::class)) as $chunk) { $encoded .= $chunk; } // encode a stringable dummy as a string $encoded = (string) $jsonEncoder->encode(new StringableDummy(), Type::string()); // encode collection with generics $type = Type::generic(Type::object(Collection::class), Type::object(Dummy::class)); $encoded = (string) $jsonEncoder->encode(new Collection([new Dummy(), new Dummy()]), $type); // decode JSON to dummy $dummy = $jsonDecoder->decode('...', Type::object(Dummy::class)); // decode JSON resource to dummy lazy ghost $resource = fopen('php://temp', 'w'); fwrite($resource, '{...}'); rewind($resource); $dummy = $jsonDecoder->decode($resource, Type::object(Dummy::class)); // decode JSON to a collection with generics $string = '{...}'; $type = Type::generic(Type::object(Collection::class), Type::object(Dummy::class); $collection = $jsonDecoder->decode($string, $type); } } ``` ## Main ideas ### Cache The main trick to improve performance is to rely on the cache. During cache warm-up (or on the fly once and for all), the data structure is computed and used to generate a cache PHP file that we could name "template". Then, the generated template is called with the actual data to deal with encoding/decoding. Template generation is the main costly thing. And because the template is computed and written once, only the template execution will be done all the other times, which implies lightning speed! Here is the workflow during runtime: | cache miss | cache hit | --------------------------------------------------------- | - | search for template | search for template | build data model | execute template file | → scalar nodes | | → collection nodes | | → object nodes | | →→ load properties metadata (reflection, attributes, ...) | | build template PHP AST | | optimize template PHP AST | | compile template PHP AST | | write template file | | execute template file | By the way, because it is intended to work mostly with DTOs, it'll work well with an automapping tool. ### Stream To improve memory usage, encoding, and decoding are relying on generators. In that way, the whole encoded data will never be at once in memory. Here is for example a simple encoding template PHP file: ```php <?php return static function (mixed $data, array $config): \Traversable { yield '{"`@id`":'; yield \json_encode($data->id); yield ',"name":'; yield \json_encode($data->name); yield '}'; }; ``` ### Configuration and context Contrary to the actual `Serializer` implementation, a difference has been made between "configuration" and "context". - The configuration is meant to be provided by the developer when calling the encoder/decoder. It is a basic hashmap such as the previous context, but is documented thanks to PHPStan types so it can be autocompleted, and validated during static analysis. - The context can be compared to runtime encoding/decoding information. It is internal and isn't meant to be manipulated by the developer. It is also a basic hashmap. ## Performance showcase With all these ideas, performance has been greatly improved. When serializing 10k objects to JSON, it is about **10 times faster** than the existing, and can even be compared to the `json_encode` native function. ![serializer_speed](https://user-images.githubusercontent.com/4955509/274238845-fce7bf92-ce24-4372-919e-69cea678db38.png) And it consumes about **2 times less memory**. ![serializer_memory](https://user-images.githubusercontent.com/4955509/274239315-557eeeb6-328f-4622-a96a-38f2c1e46d20.png) When deserializing a JSON file to a list of 50k objects, iterating one the 9999 firsts and reading the 10000th eagerly, it is more than **10 times faster** than the legacy deserialization, and can even be compared to the `json_decode` native function! ![deserializer_speed](https://user-images.githubusercontent.com/4955509/274239443-b834ed53-182d-402c-8d44-523450f6c37e.png) In terms of memory consumption, the new implementation is comparable to the existing one when reading eagerly. And when reading lazily, it consumes about **10 times less memory**! ![deserializer_memory](https://user-images.githubusercontent.com/4955509/274239753-f4b4470b-826e-434b-aac9-5dce872b6385.png) And it doesn't stop there, `@dunglas` is working on a PHP extension compatible with that new version of the component leveraging [simdjson](https://github.com/simdjson/simdjson) to make JSON serialization/deserialization even faster. These improvements will benefit several big projects such as Drupal, Sylius, and API Platform (some integration tests already have been made for this). It'll also benefit many other tiny projects as many are dealing with serialization. The code of the used benchmark can be found [here](https://github.com/mtarld/json-encoder-bench). ## Extension points ### PropertyMetadataLoaderInterface The `Symfony\Component\JsonEncoder\DataModel\{Encode,Decode}\DataModelBuilder` calls a `Symfony\Component\JsonEncoder\Mapping\PropertyMetadataLoaderInterface` to retrieve object's properties, with their name, their type, and their formatters. Therefore, it is possible to decorate (or replace) the `Symfony\Component\JsonEncoder\Mapping\PropertyMetadataLoaderInterface`. In that way, it'll be possible for example to read extra custom PHP attributes, ignore specific object's properties, and rename every properties, ... As an example, in the component, there are: - The `PropertyMetadataLoader` which reads basic properties information. - The `AttributePropertyMetadataLoader` which reads properties attributes such as `EncodedName`, `EncodeFormatter`, `DecodeFormatter`, and `MaxDepth` to ignore, rename or add formatters on the already retrieved properties. - The `GenericTypePropertyMetadataLoader` which updates properties' types according to generics, and cast date-times to strings. - The `DateTimeTypePropertyMetadataLoader` which updates properties' types to cast date-times to strings and vice-versa. For example, you can hide sensitive data of sensitive classes and a sensitive marker: ```php <?php use Symfony\Component\JsonEncoder\Mapping\PropertyMetadata; use Symfony\Component\JsonEncoder\Mapping\PropertyMetadataLoaderInterface; use Symfony\Component\TypeInfo\Type; final class CustomPropertyMetadataLoader implements PropertyMetadataLoaderInterface { public function __construct( private readonly PropertyMetadataLoaderInterface $decorated, ) { } public function load(string $className, array $config, array $context): array { $result = $this->decorated->load($className, $config, $context); if (!is_a($className, SensitiveInterface::class, true)) { return $result; } foreach ($result as &$metadata) { if ('sensitive' === $metadata->name()) { $metadata = $metadata ->withType(Type::string()) ->withFormatter(self::hideData(...)); } } $result['is_sensitive'] = new PropertyMetadata( name: 'wontBeUsed', type: Type::bool(), formatters: [self::true()], ); return $result; } public static function hideData(mixed $value): string { return hash('xxh128', json_encode($value)); } public static function true(): bool { return true; } } ``` Commits ------- aed69bb [JsonEncoder] Introduce component
2 parents 57c8040 + aed69bb commit c198130

File tree

196 files changed

+8451
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

196 files changed

+8451
-0
lines changed

.gitattributes

+2
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@
77
/src/Symfony/Component/Translation/Bridge export-ignore
88
/src/Symfony/Component/Emoji/Resources/data/* linguist-generated=true
99
/src/Symfony/Component/Intl/Resources/data/*/* linguist-generated=true
10+
/src/Symfony/Component/JsonEncoder/Tests/Fixtures/encoder/* linguist-generated=true
11+
/src/Symfony/Component/JsonEncoder/Tests/Fixtures/decoder/* linguist-generated=true
1012
/src/Symfony/**/.github/workflows/close-pull-request.yml linguist-generated=true
1113
/src/Symfony/**/.github/PULL_REQUEST_TEMPLATE.md linguist-generated=true

composer.json

+1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
"symfony/http-foundation": "self.version",
8484
"symfony/http-kernel": "self.version",
8585
"symfony/intl": "self.version",
86+
"symfony/json-encoder": "self.version",
8687
"symfony/ldap": "self.version",
8788
"symfony/lock": "self.version",
8889
"symfony/mailer": "self.version",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/Tests export-ignore
2+
/phpunit.xml.dist export-ignore
3+
/.gitattributes export-ignore
4+
/.gitignore export-ignore
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
vendor/
2+
composer.lock
3+
phpunit.xml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\JsonEncoder\Attribute;
13+
14+
/**
15+
* Defines a callable or a {@see \Symfony\Component\JsonEncoder\Decode\Denormalizer\DenormalizerInterface} service id
16+
* that will be used to denormalize the property data during decoding.
17+
*
18+
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
19+
*
20+
* @experimental
21+
*/
22+
#[\Attribute(\Attribute::TARGET_PROPERTY)]
23+
class Denormalizer
24+
{
25+
private string|\Closure $denormalizer;
26+
27+
/**
28+
* @param string|(callable(mixed, array<string, mixed>?): mixed)|(callable(mixed): mixed) $denormalizer
29+
*/
30+
public function __construct(mixed $denormalizer)
31+
{
32+
if (\is_callable($denormalizer)) {
33+
$denormalizer = \Closure::fromCallable($denormalizer);
34+
}
35+
36+
$this->denormalizer = $denormalizer;
37+
}
38+
39+
public function getDenormalizer(): string|\Closure
40+
{
41+
return $this->denormalizer;
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\JsonEncoder\Attribute;
13+
14+
/**
15+
* Defines the encoded property name.
16+
*
17+
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
18+
*
19+
* @experimental
20+
*/
21+
#[\Attribute(\Attribute::TARGET_PROPERTY)]
22+
final class EncodedName
23+
{
24+
public function __construct(
25+
private string $name,
26+
) {
27+
}
28+
29+
public function getName(): string
30+
{
31+
return $this->name;
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\JsonEncoder\Attribute;
13+
14+
/**
15+
* Defines a callable or a {@see \Symfony\Component\JsonEncoder\Encode\Normalizer\NormalizerInterface} service id
16+
* that will be used to normalize the property data during encoding.
17+
*
18+
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
19+
*
20+
* @experimental
21+
*/
22+
#[\Attribute(\Attribute::TARGET_PROPERTY)]
23+
class Normalizer
24+
{
25+
private string|\Closure $normalizer;
26+
27+
/**
28+
* @param string|(callable(mixed, array<string, mixed>?): mixed)|(callable(mixed): mixed) $normalizer
29+
*/
30+
public function __construct(mixed $normalizer)
31+
{
32+
if (\is_callable($normalizer)) {
33+
$normalizer = \Closure::fromCallable($normalizer);
34+
}
35+
36+
$this->normalizer = $normalizer;
37+
}
38+
39+
public function getNormalizer(): string|\Closure
40+
{
41+
return $this->normalizer;
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CHANGELOG
2+
=========
3+
4+
7.3
5+
---
6+
7+
* Introduce the component as experimental
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\JsonEncoder\CacheWarmer;
13+
14+
use Psr\Log\LoggerInterface;
15+
use Psr\Log\NullLogger;
16+
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
17+
use Symfony\Component\JsonEncoder\Decode\DecoderGenerator;
18+
use Symfony\Component\JsonEncoder\Encode\EncoderGenerator;
19+
use Symfony\Component\JsonEncoder\Exception\ExceptionInterface;
20+
use Symfony\Component\JsonEncoder\Mapping\PropertyMetadataLoaderInterface;
21+
use Symfony\Component\TypeInfo\Type;
22+
23+
/**
24+
* Generates encoders and decoders PHP files.
25+
*
26+
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
27+
*
28+
* @internal
29+
*/
30+
final class EncoderDecoderCacheWarmer implements CacheWarmerInterface
31+
{
32+
private EncoderGenerator $encoderGenerator;
33+
private DecoderGenerator $decoderGenerator;
34+
35+
/**
36+
* @param iterable<class-string> $encodableClassNames
37+
*/
38+
public function __construct(
39+
private iterable $encodableClassNames,
40+
PropertyMetadataLoaderInterface $encodePropertyMetadataLoader,
41+
PropertyMetadataLoaderInterface $decodePropertyMetadataLoader,
42+
string $encodersDir,
43+
string $decodersDir,
44+
bool $forceEncodeChunks = false,
45+
private LoggerInterface $logger = new NullLogger(),
46+
) {
47+
$this->encoderGenerator = new EncoderGenerator($encodePropertyMetadataLoader, $encodersDir, $forceEncodeChunks);
48+
$this->decoderGenerator = new DecoderGenerator($decodePropertyMetadataLoader, $decodersDir);
49+
}
50+
51+
public function warmUp(string $cacheDir, ?string $buildDir = null): array
52+
{
53+
foreach ($this->encodableClassNames as $className) {
54+
$type = Type::object($className);
55+
56+
$this->warmUpEncoder($type);
57+
$this->warmUpDecoders($type);
58+
}
59+
60+
return [];
61+
}
62+
63+
public function isOptional(): bool
64+
{
65+
return true;
66+
}
67+
68+
private function warmUpEncoder(Type $type): void
69+
{
70+
try {
71+
$this->encoderGenerator->generate($type);
72+
} catch (ExceptionInterface $e) {
73+
$this->logger->debug('Cannot generate "json" encoder for "{type}": {exception}', ['type' => (string) $type, 'exception' => $e]);
74+
}
75+
}
76+
77+
private function warmUpDecoders(Type $type): void
78+
{
79+
try {
80+
$this->decoderGenerator->generate($type, decodeFromStream: false);
81+
} catch (ExceptionInterface $e) {
82+
$this->logger->debug('Cannot generate "json" decoder for "{type}": {exception}', ['type' => (string) $type, 'exception' => $e]);
83+
}
84+
85+
try {
86+
$this->decoderGenerator->generate($type, decodeFromStream: true);
87+
} catch (ExceptionInterface $e) {
88+
$this->logger->debug('Cannot generate "json" streaming decoder for "{type}": {exception}', ['type' => (string) $type, 'exception' => $e]);
89+
}
90+
}
91+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\JsonEncoder\CacheWarmer;
13+
14+
use Symfony\Component\Filesystem\Filesystem;
15+
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmer;
16+
use Symfony\Component\JsonEncoder\Exception\RuntimeException;
17+
use Symfony\Component\VarExporter\ProxyHelper;
18+
19+
/**
20+
* Generates lazy ghost {@see \Symfony\Component\VarExporter\LazyGhostTrait}
21+
* PHP files for $encodable types.
22+
*
23+
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
24+
*
25+
* @internal
26+
*/
27+
final class LazyGhostCacheWarmer extends CacheWarmer
28+
{
29+
private Filesystem $fs;
30+
31+
/**
32+
* @param iterable<class-string> $encodableClassNames
33+
*/
34+
public function __construct(
35+
private iterable $encodableClassNames,
36+
private string $lazyGhostsDir,
37+
) {
38+
$this->fs = new Filesystem();
39+
}
40+
41+
public function warmUp(string $cacheDir, ?string $buildDir = null): array
42+
{
43+
if (!$this->fs->exists($this->lazyGhostsDir)) {
44+
$this->fs->mkdir($this->lazyGhostsDir);
45+
}
46+
47+
foreach ($this->encodableClassNames as $className) {
48+
$this->warmClassLazyGhost($className);
49+
}
50+
51+
return [];
52+
}
53+
54+
public function isOptional(): bool
55+
{
56+
return true;
57+
}
58+
59+
/**
60+
* @param class-string $className
61+
*/
62+
private function warmClassLazyGhost(string $className): void
63+
{
64+
$path = \sprintf('%s%s%s.php', $this->lazyGhostsDir, \DIRECTORY_SEPARATOR, hash('xxh128', $className));
65+
66+
try {
67+
$classReflection = new \ReflectionClass($className);
68+
} catch (\ReflectionException $e) {
69+
throw new RuntimeException($e->getMessage(), $e->getCode(), $e);
70+
}
71+
72+
$this->writeCacheFile($path, \sprintf(
73+
'class %s%s',
74+
\sprintf('%sGhost', preg_replace('/\\\\/', '', $className)),
75+
ProxyHelper::generateLazyGhost($classReflection),
76+
));
77+
}
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\JsonEncoder\DataModel;
13+
14+
use PhpParser\Node\Expr;
15+
16+
/**
17+
* Represents a way to access data on PHP.
18+
*
19+
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
20+
*
21+
* @internal
22+
*/
23+
interface DataAccessorInterface
24+
{
25+
/**
26+
* Converts to "nikic/php-parser" PHP expression.
27+
*/
28+
public function toPhpExpr(): Expr;
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\JsonEncoder\DataModel\Decode;
13+
14+
use Symfony\Component\TypeInfo\Type\BackedEnumType;
15+
16+
/**
17+
* Represents a backed enum in the data model graph representation.
18+
*
19+
* Backed enums are leaves in the data model tree.
20+
*
21+
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
22+
*
23+
* @internal
24+
*/
25+
final class BackedEnumNode implements DataModelNodeInterface
26+
{
27+
public function __construct(
28+
public BackedEnumType $type,
29+
) {
30+
}
31+
32+
public function getIdentifier(): string
33+
{
34+
return (string) $this->type;
35+
}
36+
37+
public function getType(): BackedEnumType
38+
{
39+
return $this->type;
40+
}
41+
}

0 commit comments

Comments
 (0)
0