Skip to content

Commit a05386b

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

File tree

246 files changed

+14849
-4
lines changed

Some content is hidden

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

246 files changed

+14849
-4
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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+
* @author Mathias Arlaud <[email protected]>
20+
*
21+
* @experimental in 7.0
22+
*/
23+
final class SerializerLazyObjectCacheWarmer extends CacheWarmer
24+
{
25+
/**
26+
* @param list<string> $serializable
27+
*/
28+
public function __construct(
29+
private readonly array $serializable,
30+
private readonly string $lazyObjectCacheDir,
31+
) {
32+
}
33+
34+
public function warmUp(string $cacheDir): array
35+
{
36+
if (!file_exists($this->lazyObjectCacheDir)) {
37+
mkdir($this->lazyObjectCacheDir, recursive: true);
38+
}
39+
40+
foreach ($this->serializable as $s) {
41+
$type = Type::fromString($s);
42+
43+
if (!$type->isObject() || !$type->hasClass()) {
44+
continue;
45+
}
46+
47+
$this->warmClassLazyObject($type->className());
48+
}
49+
50+
return [];
51+
}
52+
53+
public function isOptional(): bool
54+
{
55+
return false;
56+
}
57+
58+
/**
59+
* @param class-string $className
60+
*/
61+
private function warmClassLazyObject(string $className): void
62+
{
63+
$path = sprintf('%s%s%s.php', $this->lazyObjectCacheDir, \DIRECTORY_SEPARATOR, hash('xxh128', $className));
64+
65+
$this->writeCacheFile($path, sprintf(
66+
'class %s%s',
67+
sprintf('%sGhost', preg_replace('/\\\\/', '', $className)),
68+
ProxyHelper::generateLazyGhost(new \ReflectionClass($className)),
69+
));
70+
}
71+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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\DataModel\DataModelBuilderInterface as DeserializeDataModelBuilderInterface;
19+
use Symfony\Component\Serializer\Deserialize\Template\Template as DeserializeTemplate;
20+
use Symfony\Component\Serializer\Exception\ExceptionInterface;
21+
use Symfony\Component\Serializer\Php\VariableNode;
22+
use Symfony\Component\Serializer\Serialize\Config\SerializeConfig;
23+
use Symfony\Component\Serializer\Serialize\DataModel\DataModelBuilderInterface as SerializeDataModelBuilderInterface;
24+
use Symfony\Component\Serializer\Serialize\Template\Template as SerializeTemplate;
25+
use Symfony\Component\Serializer\Template\TemplateVariant;
26+
use Symfony\Component\Serializer\Template\TemplateVariation;
27+
use Symfony\Component\Serializer\Template\TemplateVariationExtractorInterface;
28+
use Symfony\Component\Serializer\Type\Type;
29+
30+
/**
31+
* @author Mathias Arlaud <[email protected]>
32+
*
33+
* @experimental in 7.0
34+
*/
35+
final class SerializerTemplateCacheWarmer extends CacheWarmer
36+
{
37+
/**
38+
* @param list<string> $serializable
39+
* @param list<string> $formats
40+
*/
41+
public function __construct(
42+
private readonly array $serializable,
43+
private readonly SerializeTemplate $serializeTemplate,
44+
private readonly DeserializeTemplate $deserializeTemplate,
45+
private readonly TemplateVariationExtractorInterface $templateVariationExtractor,
46+
private readonly SerializeDataModelBuilderInterface $serializeDataModelBuilder,
47+
private readonly DeserializeDataModelBuilderInterface $deserializeDataModelBuilder,
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->extractFromType($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+
$dataModel = $this->serializeDataModelBuilder->build($type, new VariableNode('data'), $variant['serialize']->config);
93+
94+
$this->writeCacheFile(
95+
$this->serializeTemplate->path($type, $format, $variant['serialize']->config),
96+
$this->serializeTemplate->content($dataModel, $format, $variant['serialize']->config),
97+
);
98+
} catch (ExceptionInterface $e) {
99+
$this->logger->debug('Cannot generate serialize "{format}" template for "{type}": {exception}', [
100+
'format' => $format,
101+
'type' => (string) $type,
102+
'exception' => $e,
103+
]);
104+
}
105+
106+
try {
107+
$dataModel = $this->deserializeDataModelBuilder->build($type, $variant['deserialize']->config);
108+
109+
$this->writeCacheFile(
110+
$this->deserializeTemplate->path($type, $format, $variant['deserialize']->config),
111+
$this->deserializeTemplate->content($dataModel, $format, $variant['deserialize']->config),
112+
);
113+
} catch (ExceptionInterface $e) {
114+
$this->logger->debug('Cannot generate deserialize "{format}" template for "{type}": {exception}', [
115+
'format' => $format,
116+
'type' => (string) $type,
117+
'exception' => $e,
118+
]);
119+
}
120+
}
121+
}
122+
123+
/**
124+
* @param list<TemplateVariation> $variations
125+
*
126+
* @return list<array{serialize: TemplateVariant, deserialize: TemplateVariant}>
127+
*/
128+
private function variants(array $variations): array
129+
{
130+
$variants = [[]];
131+
132+
foreach ($variations as $variation) {
133+
foreach ($variants as $variant) {
134+
$variants[] = array_merge([$variation], $variant);
135+
}
136+
}
137+
138+
return array_map(fn (array $variations): array => [
139+
'serialize' => new TemplateVariant(new SerializeConfig(), $variations),
140+
'deserialize' => new TemplateVariant(new DeserializeConfig(), $variations),
141+
], $variants);
142+
}
143+
}

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: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Composer\InstalledVersions;
1515
use Http\Client\HttpAsyncClient;
1616
use Http\Client\HttpClient;
17+
use Symfony\Component\Serializer\Deserialize\Instantiator\InstantiatorInterface;
1718
use phpDocumentor\Reflection\DocBlockFactoryInterface;
1819
use phpDocumentor\Reflection\Types\ContextFactory;
1920
use PhpParser\Parser;
@@ -1921,6 +1922,28 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder
19211922
if (isset($config['default_context']) && $config['default_context']) {
19221923
$container->setParameter('serializer.default_context', $config['default_context']);
19231924
}
1925+
1926+
//
1927+
// Experimental serializer
1928+
//
1929+
1930+
$container->setParameter('serializer.serializable_paths', $config['serializable_paths']);
1931+
$container->setParameter('serializer.formats', $config['formats']);
1932+
$container->setParameter('serializer.max_variants', $config['max_variants']);
1933+
1934+
$container->setParameter('serializer.lazy_instantiation', $config['lazy_instantiation']);
1935+
$container->setParameter('serializer.lazy_deserialization', $config['lazy_deserialization']);
1936+
1937+
$container->setAlias('serializer.instantiator', $config['lazy_instantiation'] ? 'serializer.instantiator.lazy' : 'serializer.instantiator.eager');
1938+
$container->setAlias(InstantiatorInterface::class, 'serializer.instantiator');
1939+
1940+
foreach ($config['serializable_paths'] as $path) {
1941+
if (!is_dir($path)) {
1942+
continue;
1943+
}
1944+
1945+
$container->fileExists($path, '/\.php$/');
1946+
}
19241947
}
19251948

19261949
private function registerPropertyInfoConfiguration(ContainerBuilder $container, PhpFileLoader $loader): void

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)