Skip to content

Commit ee2e070

Browse files
committed
[Serializer] Redesign component
1 parent f3c9644 commit ee2e070

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
@@ -86,8 +86,11 @@ class UnusedTagsPass implements CompilerPassInterface
8686
'security.expression_language_provider',
8787
'security.remember_me_handler',
8888
'security.voter',
89+
'serializer.deserialize.template_generator.eager',
90+
'serializer.deserialize.template_generator.lazy',
8991
'serializer.encoder',
9092
'serializer.normalizer',
93+
'serializer.serialize.template_generator',
9194
'texter.transport_factory',
9295
'translation.dumper',
9396
'translation.extractor',

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1091,6 +1091,8 @@ private function addSerializerSection(ArrayNodeDefinition $rootNode, callable $e
10911091
->arrayNode('serializer')
10921092
->info('serializer configuration')
10931093
->{$enableIfStandalone('symfony/serializer', Serializer::class)}()
1094+
->fixXmlConfig('serializable_path')
1095+
->fixXmlConfig('format')
10941096
->children()
10951097
->booleanNode('enable_attributes')->{!class_exists(FullStack::class) ? 'defaultTrue' : 'defaultFalse'}()->end()
10961098
->scalarNode('name_converter')->end()
@@ -1115,6 +1117,31 @@ private function addSerializerSection(ArrayNodeDefinition $rootNode, callable $e
11151117
->defaultValue([])
11161118
->prototype('variable')->end()
11171119
->end()
1120+
->arrayNode('serializable_paths')
1121+
->info('Defines where to find classes to serialized/deserialized.')
1122+
->beforeNormalization()->ifString()->then(function ($v) { return [$v]; })->end()
1123+
->prototype('scalar')->end()
1124+
->defaultValue([])
1125+
->end()
1126+
->arrayNode('formats')
1127+
->info('Defines formats to generate template during cache warm up.')
1128+
->beforeNormalization()->ifString()->then(function ($v) { return [$v]; })->end()
1129+
->prototype('scalar')->end()
1130+
->defaultValue(['json'])
1131+
->end()
1132+
->integerNode('max_variants')
1133+
->info('Defines the maximum template variants allowed for each class during cache warm up.')
1134+
->defaultValue(32)
1135+
->min(0)
1136+
->end()
1137+
->booleanNode('lazy_deserialization')
1138+
->info('Defines whether to read data lazily or eagerly.')
1139+
->defaultFalse()
1140+
->end()
1141+
->booleanNode('lazy_instantiation')
1142+
->info('Defines whether to instantiate objects lazily or eagerly.')
1143+
->defaultFalse()
1144+
->end()
11181145
->end()
11191146
->end()
11201147
->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;
@@ -372,6 +376,10 @@ public function load(array $configs, ContainerBuilder $container): void
372376
}
373377

374378
$this->registerSerializerConfiguration($config['serializer'], $container, $loader);
379+
380+
if (interface_exists(ExperimentalSerializerInterface::class)) {
381+
$this->registerExperimentalSerializerConfiguration($config['serializer'], $container, $loader);
382+
}
375383
} else {
376384
$container->getDefinition('argument_resolver.request_payload')
377385
->setArguments([])
@@ -1889,6 +1897,42 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder
18891897
}
18901898
}
18911899

1900+
private function registerExperimentalSerializerConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void
1901+
{
1902+
$loader->load('serializer_experimental.php');
1903+
1904+
$container->setParameter('serializer.serializable_paths', $config['serializable_paths']);
1905+
$container->setParameter('serializer.formats', $config['formats']);
1906+
$container->setParameter('serializer.max_variants', $config['max_variants']);
1907+
1908+
$container->setParameter('serializer.lazy_instantiation', $config['lazy_instantiation']);
1909+
$container->setParameter('serializer.lazy_deserialization', $config['lazy_deserialization']);
1910+
1911+
$container->setAlias('serializer.instantiator', $config['lazy_instantiation'] ? 'serializer.instantiator.lazy' : 'serializer.instantiator.eager');
1912+
$container->setAlias(InstantiatorInterface::class, 'serializer.instantiator');
1913+
1914+
foreach ($config['serializable_paths'] as $path) {
1915+
if (!is_dir($path)) {
1916+
continue;
1917+
}
1918+
1919+
$container->fileExists($path, '/\.php$/');
1920+
}
1921+
1922+
if (
1923+
ContainerBuilder::willBeAvailable('phpstan/phpdoc-parser', PhpDocParser::class, ['symfony/framework-bundle', 'symfony/serializer'])
1924+
&& ContainerBuilder::willBeAvailable('phpdocumentor/type-resolver', ContextFactory::class, ['symfony/framework-bundle', 'symfony/serializer'])
1925+
) {
1926+
$container->register('serializer.type_extractor.phpstan', PhpstanTypeExtractor::class)
1927+
->setDecoratedService('serializer.type_extractor')
1928+
->setArguments([
1929+
new Reference('serializer.type_extractor.phpstan.inner'),
1930+
])
1931+
->setLazy(true)
1932+
->addTag('proxy', ['interface' => TypeExtractorInterface::class]);
1933+
}
1934+
}
1935+
18921936
private function registerPropertyInfoConfiguration(ContainerBuilder $container, PhpFileLoader $loader): void
18931937
{
18941938
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
@@ -59,6 +59,8 @@
5959
use Symfony\Component\PropertyInfo\DependencyInjection\PropertyInfoPass;
6060
use Symfony\Component\Routing\DependencyInjection\RoutingResolverPass;
6161
use Symfony\Component\Scheduler\DependencyInjection\AddScheduleMessengerPass;
62+
use Symfony\Component\Serializer\DependencyInjection\RuntimeSerializerServicesPass;
63+
use Symfony\Component\Serializer\DependencyInjection\SerializablePass;
6264
use Symfony\Component\Serializer\DependencyInjection\SerializerPass;
6365
use Symfony\Component\Translation\DependencyInjection\TranslationDumperPass;
6466
use Symfony\Component\Translation\DependencyInjection\TranslationExtractorPass;
@@ -152,6 +154,9 @@ public function build(ContainerBuilder $container): void
152154
$this->addCompilerPassIfExists($container, TranslationExtractorPass::class);
153155
$this->addCompilerPassIfExists($container, TranslationDumperPass::class);
154156
$container->addCompilerPass(new FragmentRendererPass());
157+
// must be registered before the SerializerPass and RuntimeSerializerServicesPass
158+
$this->addCompilerPassIfExists($container, SerializablePass::class, priority: 16);
159+
$this->addCompilerPassIfExists($container, RuntimeSerializerServicesPass::class);
155160
$this->addCompilerPassIfExists($container, SerializerPass::class);
156161
$this->addCompilerPassIfExists($container, PropertyInfoPass::class);
157162
$container->addCompilerPass(new ControllerArgumentValueResolverPass());

0 commit comments

Comments
 (0)