Skip to content

Commit a0cd694

Browse files
committed
[Serializer] Redesign component
1 parent dbdf304 commit a0cd694

File tree

254 files changed

+15350
-2
lines changed

Some content is hidden

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

254 files changed

+15350
-2
lines changed

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
7.0
55
---
66

7+
* Add support for the experimental revamped version of the Serializer component
78
* Remove command `translation:update`, use `translation:extract` instead
89
* Make the `http_method_override` config option default to `false`
910
* Remove `AbstractController::renderForm()`, use `render()` instead
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Bundle\FrameworkBundle\CacheWarmer;
13+
14+
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmer;
15+
use Symfony\Component\Serializer\Type\Type;
16+
use Symfony\Component\VarExporter\ProxyHelper;
17+
18+
/**
19+
* Generates lazy ghost {@see Symfony\Component\VarExporter\LazyGhostTrait}
20+
* PHP files for $serializable types.
21+
*
22+
* @author Mathias Arlaud <[email protected]>
23+
*
24+
* @experimental in 7.0
25+
*/
26+
final class SerializerLazyGhostCacheWarmer extends CacheWarmer
27+
{
28+
/**
29+
* @param list<string> $serializable
30+
*/
31+
public function __construct(
32+
private readonly array $serializable,
33+
private readonly string $lazyGhostCacheDir,
34+
) {
35+
}
36+
37+
public function warmUp(string $cacheDir): array
38+
{
39+
if (!file_exists($this->lazyGhostCacheDir)) {
40+
mkdir($this->lazyGhostCacheDir, recursive: true);
41+
}
42+
43+
foreach ($this->serializable as $s) {
44+
$type = Type::fromString($s);
45+
46+
if (!$type->isObject() || !$type->hasClass()) {
47+
continue;
48+
}
49+
50+
$this->warmClassLazyGhost($type->className());
51+
}
52+
53+
return [];
54+
}
55+
56+
public function isOptional(): bool
57+
{
58+
return false;
59+
}
60+
61+
/**
62+
* @param class-string $className
63+
*/
64+
private function warmClassLazyGhost(string $className): void
65+
{
66+
$path = sprintf('%s%s%s.php', $this->lazyGhostCacheDir, \DIRECTORY_SEPARATOR, hash('xxh128', $className));
67+
68+
$this->writeCacheFile($path, sprintf(
69+
'class %s%s',
70+
sprintf('%sGhost', preg_replace('/\\\\/', '', $className)),
71+
ProxyHelper::generateLazyGhost(new \ReflectionClass($className)),
72+
));
73+
}
74+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Bundle\FrameworkBundle\CacheWarmer;
13+
14+
use Psr\Log\LoggerInterface;
15+
use Psr\Log\NullLogger;
16+
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmer;
17+
use Symfony\Component\Serializer\Deserialize\Config\DeserializeConfig;
18+
use Symfony\Component\Serializer\Deserialize\Template\Template as DeserializeTemplate;
19+
use Symfony\Component\Serializer\Exception\ExceptionInterface;
20+
use Symfony\Component\Serializer\Serialize\Config\SerializeConfig;
21+
use Symfony\Component\Serializer\Serialize\Template\Template as SerializeTemplate;
22+
use Symfony\Component\Serializer\Template\TemplateVariant;
23+
use Symfony\Component\Serializer\Template\TemplateVariation;
24+
use Symfony\Component\Serializer\Template\TemplateVariationExtractorInterface;
25+
use Symfony\Component\Serializer\Type\Type;
26+
27+
/**
28+
* Generates serialization and deserialization templates PHP files.
29+
*
30+
* It generates templates for each $formats and each variants
31+
* of $serializable types limited to $maxVariants.
32+
*
33+
* @author Mathias Arlaud <[email protected]>
34+
*
35+
* @experimental in 7.0
36+
*/
37+
final class SerializerTemplateCacheWarmer extends CacheWarmer
38+
{
39+
/**
40+
* @param list<string> $serializable
41+
* @param list<string> $formats
42+
*/
43+
public function __construct(
44+
private readonly array $serializable,
45+
private readonly SerializeTemplate $serializeTemplate,
46+
private readonly DeserializeTemplate $deserializeTemplate,
47+
private readonly TemplateVariationExtractorInterface $templateVariationExtractor,
48+
private readonly string $templateCacheDir,
49+
private readonly array $formats,
50+
private readonly int $maxVariants,
51+
private readonly LoggerInterface $logger = new NullLogger(),
52+
) {
53+
}
54+
55+
public function warmUp(string $cacheDir): array
56+
{
57+
if (!file_exists($this->templateCacheDir)) {
58+
mkdir($this->templateCacheDir, recursive: true);
59+
}
60+
61+
foreach ($this->serializable as $s) {
62+
$type = Type::fromString($s);
63+
64+
$variations = $this->templateVariationExtractor->extractVariationsFromType($type);
65+
$variants = $this->variants($variations);
66+
67+
if (\count($variants) > $this->maxVariants) {
68+
$this->logger->debug('Too many variants for "{type}", keeping only the first {maxVariants}.', ['type' => $s, 'maxVariants' => $this->maxVariants]);
69+
$variants = \array_slice($variants, offset: 0, length: $this->maxVariants);
70+
}
71+
72+
foreach ($this->formats as $format) {
73+
$this->warmTemplates($type, $variants, $format);
74+
}
75+
}
76+
77+
return [];
78+
}
79+
80+
public function isOptional(): bool
81+
{
82+
return false;
83+
}
84+
85+
/**
86+
* @param list<array{serialize: TemplateVariant, deserialize: TemplateVariant}> $variants
87+
*/
88+
private function warmTemplates(Type $type, array $variants, string $format): void
89+
{
90+
foreach ($variants as $variant) {
91+
try {
92+
$this->writeCacheFile(
93+
$this->serializeTemplate->path($type, $format, $variant['serialize']->config),
94+
$this->serializeTemplate->content($type, $format, $variant['serialize']->config),
95+
);
96+
} catch (ExceptionInterface $e) {
97+
$this->logger->debug('Cannot generate serialize "{format}" template for "{type}": {exception}', [
98+
'format' => $format,
99+
'type' => (string) $type,
100+
'exception' => $e,
101+
]);
102+
}
103+
104+
try {
105+
$this->writeCacheFile(
106+
$this->deserializeTemplate->path($type, $format, $variant['deserialize']->config),
107+
$this->deserializeTemplate->content($type, $format, $variant['deserialize']->config),
108+
);
109+
} catch (ExceptionInterface $e) {
110+
$this->logger->debug('Cannot generate deserialize "{format}" template for "{type}": {exception}', [
111+
'format' => $format,
112+
'type' => (string) $type,
113+
'exception' => $e,
114+
]);
115+
}
116+
}
117+
}
118+
119+
/**
120+
* @param list<TemplateVariation> $variations
121+
*
122+
* @return list<array{serialize: TemplateVariant, deserialize: TemplateVariant}>
123+
*/
124+
private function variants(array $variations): array
125+
{
126+
$variants = [[]];
127+
128+
foreach ($variations as $variation) {
129+
foreach ($variants as $variant) {
130+
$variants[] = array_merge([$variation], $variant);
131+
}
132+
}
133+
134+
return array_map(fn (array $variations): array => [
135+
'serialize' => new TemplateVariant(new SerializeConfig(), $variations),
136+
'deserialize' => new TemplateVariant(new DeserializeConfig(), $variations),
137+
], $variants);
138+
}
139+
}

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,11 @@ class UnusedTagsPass implements CompilerPassInterface
8787
'security.expression_language_provider',
8888
'security.remember_me_handler',
8989
'security.voter',
90+
'serializer.deserialize.template_generator.eager',
91+
'serializer.deserialize.template_generator.lazy',
9092
'serializer.encoder',
9193
'serializer.normalizer',
94+
'serializer.serialize.template_generator',
9295
'texter.transport_factory',
9396
'translation.dumper',
9497
'translation.extractor',

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,6 +1087,8 @@ private function addSerializerSection(ArrayNodeDefinition $rootNode, callable $e
10871087
->arrayNode('serializer')
10881088
->info('serializer configuration')
10891089
->{$enableIfStandalone('symfony/serializer', Serializer::class)}()
1090+
->fixXmlConfig('serializable_path')
1091+
->fixXmlConfig('format')
10901092
->children()
10911093
->booleanNode('enable_annotations')->{!class_exists(FullStack::class) ? 'defaultTrue' : 'defaultFalse'}()->end()
10921094
->scalarNode('name_converter')->end()
@@ -1111,6 +1113,31 @@ private function addSerializerSection(ArrayNodeDefinition $rootNode, callable $e
11111113
->defaultValue([])
11121114
->prototype('variable')->end()
11131115
->end()
1116+
->arrayNode('serializable_paths')
1117+
->info('Defines where to find classes to serialized/deserialized.')
1118+
->beforeNormalization()->ifString()->then(function ($v) { return [$v]; })->end()
1119+
->prototype('scalar')->end()
1120+
->defaultValue([])
1121+
->end()
1122+
->arrayNode('formats')
1123+
->info('Defines formats to generate template during cache warm up.')
1124+
->beforeNormalization()->ifString()->then(function ($v) { return [$v]; })->end()
1125+
->prototype('scalar')->end()
1126+
->defaultValue(['json'])
1127+
->end()
1128+
->integerNode('max_variants')
1129+
->info('Defines the maximum template variants allowed for each class during cache warm up.')
1130+
->defaultValue(32)
1131+
->min(0)
1132+
->end()
1133+
->booleanNode('lazy_deserialization')
1134+
->info('Defines whether to read data lazily or eagerly.')
1135+
->defaultFalse()
1136+
->end()
1137+
->booleanNode('lazy_instantiation')
1138+
->info('Defines whether to instantiate objects lazily or eagerly.')
1139+
->defaultFalse()
1140+
->end()
11141141
->end()
11151142
->end()
11161143
->end()

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
use Composer\InstalledVersions;
1515
use Http\Client\HttpAsyncClient;
1616
use Http\Client\HttpClient;
17+
use Symfony\Component\Serializer\Deserialize\Instantiator\InstantiatorInterface;
18+
use Symfony\Component\Serializer\Serialize\SerializerInterface as ExperimentalSerializerInterface;
19+
use Symfony\Component\Serializer\Type\PhpstanTypeExtractor;
20+
use Symfony\Component\Serializer\Type\TypeExtractorInterface;
1721
use phpDocumentor\Reflection\DocBlockFactoryInterface;
1822
use phpDocumentor\Reflection\Types\ContextFactory;
1923
use PhpParser\Parser;
@@ -388,6 +392,10 @@ public function load(array $configs, ContainerBuilder $container): void
388392
}
389393

390394
$this->registerSerializerConfiguration($config['serializer'], $container, $loader);
395+
396+
if (interface_exists(ExperimentalSerializerInterface::class)) {
397+
$this->registerExperimentalSerializerConfiguration($config['serializer'], $container, $loader);
398+
}
391399
} else {
392400
$container->getDefinition('argument_resolver.request_payload')
393401
->setArguments([])
@@ -1923,6 +1931,42 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder
19231931
}
19241932
}
19251933

1934+
private function registerExperimentalSerializerConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void
1935+
{
1936+
$loader->load('serializer_experimental.php');
1937+
1938+
$container->setParameter('serializer.serializable_paths', $config['serializable_paths']);
1939+
$container->setParameter('serializer.formats', $config['formats']);
1940+
$container->setParameter('serializer.max_variants', $config['max_variants']);
1941+
1942+
$container->setParameter('serializer.lazy_instantiation', $config['lazy_instantiation']);
1943+
$container->setParameter('serializer.lazy_deserialization', $config['lazy_deserialization']);
1944+
1945+
$container->setAlias('serializer.instantiator', $config['lazy_instantiation'] ? 'serializer.instantiator.lazy' : 'serializer.instantiator.eager');
1946+
$container->setAlias(InstantiatorInterface::class, 'serializer.instantiator');
1947+
1948+
foreach ($config['serializable_paths'] as $path) {
1949+
if (!is_dir($path)) {
1950+
continue;
1951+
}
1952+
1953+
$container->fileExists($path, '/\.php$/');
1954+
}
1955+
1956+
if (
1957+
ContainerBuilder::willBeAvailable('phpstan/phpdoc-parser', PhpDocParser::class, ['symfony/framework-bundle', 'symfony/serializer'])
1958+
&& ContainerBuilder::willBeAvailable('phpdocumentor/type-resolver', ContextFactory::class, ['symfony/framework-bundle', 'symfony/serializer'])
1959+
) {
1960+
$container->register('serializer.type_extractor.phpstan', PhpstanTypeExtractor::class)
1961+
->setDecoratedService('serializer.type_extractor')
1962+
->setArguments([
1963+
new Reference('serializer.type_extractor.phpstan.inner'),
1964+
])
1965+
->setLazy(true)
1966+
->addTag('proxy', ['interface' => TypeExtractorInterface::class]);
1967+
}
1968+
}
1969+
19261970
private function registerPropertyInfoConfiguration(ContainerBuilder $container, PhpFileLoader $loader): void
19271971
{
19281972
if (!interface_exists(PropertyInfoExtractorInterface::class)) {

src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@
6060
use Symfony\Component\PropertyInfo\DependencyInjection\PropertyInfoPass;
6161
use Symfony\Component\Routing\DependencyInjection\RoutingResolverPass;
6262
use Symfony\Component\Scheduler\DependencyInjection\AddScheduleMessengerPass;
63+
use Symfony\Component\Serializer\DependencyInjection\RuntimeSerializerServicesPass;
64+
use Symfony\Component\Serializer\DependencyInjection\SerializablePass;
6365
use Symfony\Component\Serializer\DependencyInjection\SerializerPass;
6466
use Symfony\Component\Translation\DependencyInjection\TranslationDumperPass;
6567
use Symfony\Component\Translation\DependencyInjection\TranslationExtractorPass;
@@ -154,6 +156,9 @@ public function build(ContainerBuilder $container): void
154156
$this->addCompilerPassIfExists($container, TranslationExtractorPass::class);
155157
$this->addCompilerPassIfExists($container, TranslationDumperPass::class);
156158
$container->addCompilerPass(new FragmentRendererPass());
159+
// must be registered before the SerializerPass and RuntimeSerializerServicesPass
160+
$this->addCompilerPassIfExists($container, SerializablePass::class, priority: 16);
161+
$this->addCompilerPassIfExists($container, RuntimeSerializerServicesPass::class);
157162
$this->addCompilerPassIfExists($container, SerializerPass::class);
158163
$this->addCompilerPassIfExists($container, PropertyInfoPass::class);
159164
$container->addCompilerPass(new ControllerArgumentValueResolverPass());

0 commit comments

Comments
 (0)