diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerNormalizerChooserCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerNormalizerChooserCacheWarmer.php new file mode 100644 index 0000000000000..d7e5a4b450ab2 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerNormalizerChooserCacheWarmer.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\CacheWarmer; + +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Serializer\Cache\CacheNormalizationProviderInterface; +use Symfony\Component\Serializer\Normalizer\Chooser\CacheNormalizerChooser; +use Symfony\Component\Serializer\Normalizer\Chooser\NormalizerChooser; +use Symfony\Component\Serializer\Normalizer\Chooser\NormalizerChooserInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +class SerializerNormalizerChooserCacheWarmer extends AbstractPhpFileCacheWarmer +{ + private $normalizers; + private $normalizationProviders; + private $decoratedNormalizerChooser; + + public function __construct(array $normalizers, array $normalizationProviders, string $phpArrayFile, NormalizerChooserInterface $decoratedNormalizerChooser) + { + parent::__construct($phpArrayFile); + $this->normalizers = $normalizers; + $this->normalizationProviders = $normalizationProviders; + $this->decoratedNormalizerChooser = $decoratedNormalizerChooser; + } + + protected function doWarmUp(string $cacheDir, ArrayAdapter $arrayAdapter) + { + $normalizerChooser = new CacheNormalizerChooser($this->decoratedNormalizerChooser, $arrayAdapter); + + foreach ($this->normalizationProviders as $normalizationProvider) { + if (!$normalizationProvider instanceof CacheNormalizationProviderInterface) { + continue; + } + + foreach ($normalizationProvider->provide() as $normalizationContext) { + $format = $normalizationContext[0]; + $data = $normalizationContext[1]; + $context = $normalizationContext[2] ?? []; + + $normalizerChooser->chooseNormalizer($this->normalizers, $data, $format, $context); + $normalizerChooser->chooseDenormalizer($this->normalizers, $data, get_class($data), $format, $context); + } + } + + return true; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/SerializerNormalizationCachePass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/SerializerNormalizationCachePass.php new file mode 100644 index 0000000000000..c57c631a07997 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/SerializerNormalizationCachePass.php @@ -0,0 +1,32 @@ +cacheWarmerService = $cacheWarmerService; + $this->normalizerTag = $normalizerTag; + $this->cacheNormalizationProviderTag = $cacheNormalizationProviderTag; + } + + public function process(ContainerBuilder $container) + { + $cacheWarmer = $container->getDefinition($this->cacheWarmerService); + $normalizers = $this->findAndSortTaggedServices($this->normalizerTag, $container); + $providers = $this->findAndSortTaggedServices($this->cacheNormalizationProviderTag, $container); + $cacheWarmer->setArgument(0, $normalizers); + $cacheWarmer->setArgument(1, $providers); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index d8325dd39499c..d964a1dc4e881 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -139,6 +139,7 @@ use Symfony\Component\Routing\Loader\AnnotationFileLoader; use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Symfony\Component\Serializer\Cache\CacheNormalizationProviderInterface; use Symfony\Component\Serializer\Encoder\DecoderInterface; use Symfony\Component\Serializer\Encoder\EncoderInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; @@ -525,6 +526,8 @@ public function load(array $configs, ContainerBuilder $container) ->addTag('mime.mime_type_guesser'); $container->registerForAutoconfiguration(LoggerAwareInterface::class) ->addMethodCall('setLogger', [new Reference('logger')]); + $container->registerForAutoconfiguration(CacheNormalizationProviderInterface::class) + ->addTag('serializer.normalizer_chooser.cache.provider'); if (!$container->getParameter('kernel.debug')) { // remove tagged iterator argument for resource checkers diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index 7f439bb572f87..f0884bb30d30d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -20,6 +20,7 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\LoggingTranslatorPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ProfilerPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\RemoveUnusedSessionMarshallingHandlerPass; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\SerializerNormalizationCachePass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\SessionPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerRealRefPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerWeakRefPass; @@ -140,6 +141,7 @@ public function build(ContainerBuilder $container) $this->addCompilerPassIfExists($container, TranslationDumperPass::class); $container->addCompilerPass(new FragmentRendererPass()); $this->addCompilerPassIfExists($container, SerializerPass::class); + $this->addCompilerPassIfExists($container, SerializerNormalizationCachePass::class); $this->addCompilerPassIfExists($container, PropertyInfoPass::class); $container->addCompilerPass(new DataCollectorTranslatorPass()); $container->addCompilerPass(new ControllerArgumentValueResolverPass()); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php index e4868c054aea1..1838087f0c86a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php @@ -13,6 +13,7 @@ use Psr\Cache\CacheItemPoolInterface; use Symfony\Bundle\FrameworkBundle\CacheWarmer\SerializerCacheWarmer; +use Symfony\Bundle\FrameworkBundle\CacheWarmer\SerializerNormalizerChooserCacheWarmer; use Symfony\Component\Cache\Adapter\PhpArrayAdapter; use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer; use Symfony\Component\ErrorHandler\ErrorRenderer\SerializerErrorRenderer; @@ -32,6 +33,9 @@ use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\Chooser\CacheNormalizerChooser; +use Symfony\Component\Serializer\Normalizer\Chooser\NormalizerChooser; +use Symfony\Component\Serializer\Normalizer\Chooser\NormalizerChooserInterface; use Symfony\Component\Serializer\Normalizer\ConstraintViolationListNormalizer; use Symfony\Component\Serializer\Normalizer\DataUriNormalizer; use Symfony\Component\Serializer\Normalizer\DateIntervalNormalizer; @@ -52,13 +56,15 @@ return static function (ContainerConfigurator $container) { $container->parameters() - ->set('serializer.mapping.cache.file', '%kernel.cache_dir%/serialization.php') + ->set('serializer.mapping.cache.file', '%kernel.cache_dir%/serialization.mapping.php') + ->set('serializer.normalizer_chooser.cache.file', '%kernel.cache_dir%/serialization.normalization.php') ; $container->services() ->set('serializer', Serializer::class) ->public() ->args([[], []]) + ->call('setNormalizerChooser', [service('serializer.normalizer_chooser')]) ->tag('container.private', ['package' => 'symfony/framework-bundle', 'version' => '5.2']) ->alias(SerializerInterface::class, 'serializer') @@ -143,6 +149,12 @@ ->set('serializer.denormalizer.array', ArrayDenormalizer::class) ->tag('serializer.normalizer', ['priority' => -990]) + // Normalizer chooser + ->set('serializer.normalizer_chooser', NormalizerChooser::class) + ->args([service('serializer'), service('serializer')]) + + ->alias(NormalizerChooserInterface::class, 'serializer.normalizer_chooser') + // Loader ->set('serializer.mapping.chain_loader', LoaderChain::class) ->args([[]]) @@ -169,6 +181,26 @@ service('serializer.mapping.cache.symfony'), ]) + ->set('serializer.normalizer_chooser.cache_warmer', SerializerNormalizerChooserCacheWarmer::class) + ->args([ + [], + abstract_arg('The normalization providers'), + param('serializer.normalizer_chooser.cache.file'), + service('serializer.normalizer_chooser.cache.inner'), + ]) + ->tag('kernel.cache_warmer') + + ->set('serializer.normalizer_chooser.cache.symfony', CacheItemPoolInterface::class) + ->factory([PhpArrayAdapter::class, 'create']) + ->args([param('serializer.normalizer_chooser.cache.file'), service('cache.serializer')]) + + ->set('serializer.normalizer_chooser.cache', CacheNormalizerChooser::class) + ->decorate('serializer.normalizer_chooser') + ->args([ + service('serializer.normalizer_chooser.cache.inner'), + service('serializer.normalizer_chooser.cache.symfony') + ]) + // Encoders ->set('serializer.encoder.xml', XmlEncoder::class) ->tag('serializer.encoder') diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/SerializerNormalizerChooserCacheWarmerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/SerializerNormalizerChooserCacheWarmerTest.php new file mode 100644 index 0000000000000..d642c13c854e0 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/SerializerNormalizerChooserCacheWarmerTest.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\CacheWarmer; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\CacheWarmer\SerializerNormalizerChooserCacheWarmer; +use Symfony\Component\Cache\Adapter\NullAdapter; +use Symfony\Component\Cache\Adapter\PhpArrayAdapter; +use Symfony\Component\Serializer\Cache\CacheNormalizationProviderInterface; +use Symfony\Component\Serializer\Normalizer\Chooser\NormalizerChooserInterface; +use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; + +class SerializerNormalizerChooserCacheWarmerTest extends TestCase +{ + public function testWarmUp() + { + $file = sys_get_temp_dir().'/serializer.normalization.php'; + @unlink($file); + + $provider1 = $this->createMock(CacheNormalizationProviderInterface::class); + $provider1->method('provide')->willReturnCallback(function () { + yield ['json', new \stdClass()]; + yield ['xml', new \stdClass(), ['foo' => 'bar']]; + yield ['xml', new \DateTime()]; + }); + + $provider2 = $this->createMock(CacheNormalizationProviderInterface::class); + $provider2->method('provide')->willReturnCallback(function () { + yield ['yaml', new \stdClass()]; + yield ['json', new \DateTime()]; + }); + + $dateTimeNormalizer = new DateTimeNormalizer(); + $objectNormalizer = new ObjectNormalizer(); + + $decoratedNormalizerChooser = $this->createMock(NormalizerChooserInterface::class); + $decoratedNormalizerChooser->method('chooseNormalizer')->willReturn(); + $decoratedNormalizerChooser->method('chooseDenormalizer')->willReturn(); + + $cacheWarmer = new SerializerNormalizerChooserCacheWarmer( + [$dateTimeNormalizer, $objectNormalizer], + [$provider1, $provider2], + $file + ); + + $cacheWarmer->warmUp(\dirname($file)); + $this->assertFileExists($file); + + $arrayPool = new PhpArrayAdapter($file, new NullAdapter()); + $compiledContext = hash('md5', json_encode(['foo' => 'bar'])); + + foreach (['normalizer', 'denormalizer'] as $action) { + $this->assertSame(1, $arrayPool->getItem("{$action}_json_stdClass")->get()); + $this->assertSame(1, $arrayPool->getItem("{$action}_xml_stdClass_$compiledContext")->get()); + $this->assertSame(0, $arrayPool->getItem("{$action}_xml_DateTime")->get()); + $this->assertSame(1, $arrayPool->getItem("{$action}_yaml_stdClass")->get()); + $this->assertSame(0, $arrayPool->getItem("{$action}_json_DateTime")->get()); + } + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 5cd05b5c6a21a..50e952d96aa5a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -53,7 +53,7 @@ "symfony/security-bundle": "^5.1", "symfony/security-csrf": "^4.4|^5.0", "symfony/security-http": "^4.4|^5.0", - "symfony/serializer": "^5.2", + "symfony/serializer": "^5.3", "symfony/stopwatch": "^4.4|^5.0", "symfony/string": "^5.0", "symfony/translation": "^5.0", @@ -85,7 +85,7 @@ "symfony/mime": "<4.4", "symfony/property-info": "<4.4", "symfony/property-access": "<5.2", - "symfony/serializer": "<5.2", + "symfony/serializer": "<5.3", "symfony/stopwatch": "<4.4", "symfony/translation": "<5.0", "symfony/twig-bridge": "<4.4", diff --git a/src/Symfony/Component/Serializer/Cache/CacheNormalizationProviderInterface.php b/src/Symfony/Component/Serializer/Cache/CacheNormalizationProviderInterface.php new file mode 100644 index 0000000000000..c25b6b117600a --- /dev/null +++ b/src/Symfony/Component/Serializer/Cache/CacheNormalizationProviderInterface.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Cache; + +interface CacheNormalizationProviderInterface +{ + public function provide(): \Generator; +} diff --git a/src/Symfony/Component/Serializer/Normalizer/Chooser/CacheNormalizerChooser.php b/src/Symfony/Component/Serializer/Normalizer/Chooser/CacheNormalizerChooser.php new file mode 100644 index 0000000000000..5229c3df08cee --- /dev/null +++ b/src/Symfony/Component/Serializer/Normalizer/Chooser/CacheNormalizerChooser.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Normalizer\Chooser; + +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +class CacheNormalizerChooser implements NormalizerChooserInterface +{ + private $decorated; + private $cacheItemPool; + private $loadedNormalizers; + private $loadedDenormalizers; + + public function __construct(NormalizerChooserInterface $decorated, CacheItemPoolInterface $cacheItemPool) + { + $this->decorated = $decorated; + $this->cacheItemPool = $cacheItemPool; + } + + public function chooseNormalizer(array $normalizers, $data, ?string $format = null, array $context = []): ?NormalizerInterface + { + $type = \is_object($data) ? \get_class($data) : 'native-'.\gettype($data); + $type = str_replace('\\', '-', $type); + $key = $this->generateKey(true, $type, $format, $context); + + if (isset($this->loadedNormalizers[$key])) { + return $this->loadedNormalizers[$key]; + } + + $item = $this->cacheItemPool->getItem($key); + + if ($item->isHit()) { + return $this->loadedNormalizers[$key] = $normalizers[$item->get()]; + } + + $normalizer = $this->loadedNormalizers[$key] = $this->decorated->chooseNormalizer($normalizers, $data, $format, $context); + $this->cacheItemPool->save($item->set(array_search($normalizer, $normalizers))); + + return $normalizer; + } + + public function chooseDenormalizer(array $denormalizers, $data, string $class, ?string $format = null, array $context = []): ?DenormalizerInterface + { + $type = str_replace('\\', '-', $class); + $key = $this->generateKey(false, $type, $format, $context); + + if (isset($this->loadedDenormalizers[$key])) { + return $this->loadedDenormalizers[$key]; + } + + $item = $this->cacheItemPool->getItem($key); + + if ($item->isHit()) { + return $this->loadedDenormalizers[$key] = $denormalizers[$item->get()]; + } + + $denormalizer = $this->loadedDenormalizers[$key] = $this->decorated->chooseDenormalizer($denormalizers, $data, $class, $format, $context); + $this->cacheItemPool->save($item->set(array_search($denormalizer, $denormalizers))); + + return $denormalizer; + } + + private function generateKey(bool $normalize, string $type, ?string $format, array $context = []): string + { + $compiledFormat = null !== $format ? '_'.$format : ''; + $compiledContext = count($context) ? '_'.hash('md5', json_encode($context)) : ''; + + return sprintf('%s%s_%s%s', $normalize ? 'normalizer' : 'denormalizer', $compiledFormat, $type, $compiledContext); + } +} diff --git a/src/Symfony/Component/Serializer/Normalizer/Chooser/NormalizerChooser.php b/src/Symfony/Component/Serializer/Normalizer/Chooser/NormalizerChooser.php new file mode 100644 index 0000000000000..f0d2aef4b6a9e --- /dev/null +++ b/src/Symfony/Component/Serializer/Normalizer/Chooser/NormalizerChooser.php @@ -0,0 +1,117 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Normalizer\Chooser; + +use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\SerializerAwareInterface; +use Symfony\Component\Serializer\SerializerInterface; + +class NormalizerChooser implements NormalizerChooserInterface +{ + private $normalizerCache = []; + private $denormalizerCache = []; + private $normalizer; + private $denormalizer; + private $serializer; + + public function __construct(NormalizerInterface $normalizer, DenormalizerInterface $denormalizer, SerializerInterface $serializer) + { + $this->normalizer = $normalizer; + $this->denormalizer = $denormalizer; + $this->serializer = $serializer; + } + + public function chooseNormalizer(array $normalizers, $data, ?string $format = null, array $context = []): ?NormalizerInterface + { + $type = \is_object($data) ? \get_class($data) : 'native-'.\gettype($data); + + if (!isset($this->normalizerCache[$format][$type])) { + $this->normalizerCache[$format][$type] = []; + + foreach ($normalizers as $key => $normalizer) { + if (!$normalizer instanceof NormalizerInterface) { + continue; + } + + $normalizer = $this->prepareNormalizer($normalizer); + + if (!$normalizer instanceof CacheableSupportsMethodInterface || !$normalizer->hasCacheableSupportsMethod()) { + $this->normalizerCache[$format][$type][$key] = false; + } elseif ($normalizer->supportsNormalization($data, $format, $context)) { + $this->normalizerCache[$format][$type][$key] = true; + break; + } + } + } + + foreach ($this->normalizerCache[$format][$type] as $key => $cached) { + $normalizer = $normalizers[$key]; + if ($cached || $normalizer->supportsNormalization($data, $format, $context)) { + return $normalizer; + } + } + + return null; + } + + public function chooseDenormalizer(array $denormalizers, $data, string $class, ?string $format = null, array $context = []): ?DenormalizerInterface + { + if (!isset($this->denormalizerCache[$format][$class])) { + $this->denormalizerCache[$format][$class] = []; + + foreach ($denormalizers as $key => $denormalizer) { + if (!$denormalizer instanceof DenormalizerInterface) { + continue; + } + + $denormalizer = $this->prepareNormalizer($denormalizer); + + if (!$denormalizer instanceof CacheableSupportsMethodInterface || !$denormalizer->hasCacheableSupportsMethod()) { + $this->denormalizerCache[$format][$class][$key] = false; + } elseif ($denormalizer->supportsDenormalization(null, $class, $format, $context)) { + $this->denormalizerCache[$format][$class][$key] = true; + break; + } + } + } + + foreach ($this->denormalizerCache[$format][$class] as $key => $cached) { + $denormalizer = $denormalizers[$key]; + if ($cached || $denormalizer->supportsDenormalization($data, $class, $format, $context)) { + return $denormalizer; + } + } + + return null; + } + + private function prepareNormalizer($normalizer) + { + if ($normalizer instanceof NormalizerAwareInterface) { + $normalizer->setNormalizer($this->normalizer); + } + + if ($normalizer instanceof DenormalizerAwareInterface) { + $normalizer->setDenormalizer($this->denormalizer); + } + + if ($normalizer instanceof SerializerAwareInterface) { + $normalizer->setSerializer($this->serializer); + } + + return $normalizer; + } +} diff --git a/src/Symfony/Component/Serializer/Normalizer/Chooser/NormalizerChooserInterface.php b/src/Symfony/Component/Serializer/Normalizer/Chooser/NormalizerChooserInterface.php new file mode 100644 index 0000000000000..4aac1d5cefd39 --- /dev/null +++ b/src/Symfony/Component/Serializer/Normalizer/Chooser/NormalizerChooserInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Normalizer\Chooser; + +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +interface NormalizerChooserInterface +{ + public function chooseNormalizer(array $normalizers, $data, ?string $format = null, array $context = []): ?NormalizerInterface; + + public function chooseDenormalizer(array $denormalizers, $data, string $class, ?string $format = null, array $context = []): ?DenormalizerInterface; +} diff --git a/src/Symfony/Component/Serializer/Serializer.php b/src/Symfony/Component/Serializer/Serializer.php index 6414caf900472..8e3119f812051 100644 --- a/src/Symfony/Component/Serializer/Serializer.php +++ b/src/Symfony/Component/Serializer/Serializer.php @@ -23,6 +23,8 @@ use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; +use Symfony\Component\Serializer\Normalizer\Chooser\NormalizerChooser; +use Symfony\Component\Serializer\Normalizer\Chooser\NormalizerChooserInterface; use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface; use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; @@ -67,6 +69,7 @@ class Serializer implements SerializerInterface, ContextAwareNormalizerInterface private $normalizers = []; private $denormalizerCache = []; private $normalizerCache = []; + private $normalizerChooser; /** * @param (NormalizerInterface|DenormalizerInterface)[] $normalizers @@ -112,6 +115,12 @@ public function __construct(array $normalizers = [], array $encoders = []) } $this->encoder = new ChainEncoder($realEncoders); $this->decoder = new ChainDecoder($decoders); + $this->normalizerChooser = new NormalizerChooser($this, $this); + } + + public function setNormalizerChooser(NormalizerChooserInterface $normalizerChooser): void + { + $this->normalizerChooser = $normalizerChooser; } /** @@ -150,7 +159,7 @@ final public function deserialize($data, string $type, string $format, array $co public function normalize($data, string $format = null, array $context = []) { // If a normalizer supports the given data, use it - if ($normalizer = $this->getNormalizer($data, $format, $context)) { + if ($normalizer = $this->normalizerChooser->chooseNormalizer($this->normalizers, $data, $format, $context)) { return $normalizer->normalize($data, $format, $context); } @@ -189,7 +198,7 @@ public function normalize($data, string $format = null, array $context = []) */ public function denormalize($data, string $type, string $format = null, array $context = []) { - $normalizer = $this->getDenormalizer($data, $type, $format, $context); + $normalizer = $this->normalizerChooser->chooseDenormalizer($this->normalizers, $data, $type, $format, $context); // Check for a denormalizer first, e.g. the data is wrapped if (!$normalizer && isset(self::SCALAR_TYPES[$type])) { @@ -216,7 +225,7 @@ public function denormalize($data, string $type, string $format = null, array $c */ public function supportsNormalization($data, string $format = null, array $context = []) { - return null !== $this->getNormalizer($data, $format, $context); + return null !== $this->normalizerChooser->chooseNormalizer($this->normalizers, $data, $format, $context); } /** @@ -224,82 +233,7 @@ public function supportsNormalization($data, string $format = null, array $conte */ public function supportsDenormalization($data, string $type, string $format = null, array $context = []) { - return isset(self::SCALAR_TYPES[$type]) || null !== $this->getDenormalizer($data, $type, $format, $context); - } - - /** - * Returns a matching normalizer. - * - * @param mixed $data Data to get the serializer for - * @param string $format Format name, present to give the option to normalizers to act differently based on formats - * @param array $context Options available to the normalizer - */ - private function getNormalizer($data, ?string $format, array $context): ?NormalizerInterface - { - $type = \is_object($data) ? \get_class($data) : 'native-'.\gettype($data); - - if (!isset($this->normalizerCache[$format][$type])) { - $this->normalizerCache[$format][$type] = []; - - foreach ($this->normalizers as $k => $normalizer) { - if (!$normalizer instanceof NormalizerInterface) { - continue; - } - - if (!$normalizer instanceof CacheableSupportsMethodInterface || !$normalizer->hasCacheableSupportsMethod()) { - $this->normalizerCache[$format][$type][$k] = false; - } elseif ($normalizer->supportsNormalization($data, $format, $context)) { - $this->normalizerCache[$format][$type][$k] = true; - break; - } - } - } - - foreach ($this->normalizerCache[$format][$type] as $k => $cached) { - $normalizer = $this->normalizers[$k]; - if ($cached || $normalizer->supportsNormalization($data, $format, $context)) { - return $normalizer; - } - } - - return null; - } - - /** - * Returns a matching denormalizer. - * - * @param mixed $data Data to restore - * @param string $class The expected class to instantiate - * @param string $format Format name, present to give the option to normalizers to act differently based on formats - * @param array $context Options available to the denormalizer - */ - private function getDenormalizer($data, string $class, ?string $format, array $context): ?DenormalizerInterface - { - if (!isset($this->denormalizerCache[$format][$class])) { - $this->denormalizerCache[$format][$class] = []; - - foreach ($this->normalizers as $k => $normalizer) { - if (!$normalizer instanceof DenormalizerInterface) { - continue; - } - - if (!$normalizer instanceof CacheableSupportsMethodInterface || !$normalizer->hasCacheableSupportsMethod()) { - $this->denormalizerCache[$format][$class][$k] = false; - } elseif ($normalizer->supportsDenormalization(null, $class, $format, $context)) { - $this->denormalizerCache[$format][$class][$k] = true; - break; - } - } - } - - foreach ($this->denormalizerCache[$format][$class] as $k => $cached) { - $normalizer = $this->normalizers[$k]; - if ($cached || $normalizer->supportsDenormalization($data, $class, $format, $context)) { - return $normalizer; - } - } - - return null; + return isset(self::SCALAR_TYPES[$type]) || null !== $this->normalizerChooser->chooseDenormalizer($this->normalizers, $data, $type, $format, $context); } /** diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/Chooser/NormalizerChooserTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/Chooser/NormalizerChooserTest.php new file mode 100644 index 0000000000000..2660a05150074 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/Chooser/NormalizerChooserTest.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Normalizer\Chooser; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Serializer\Normalizer\Chooser\NormalizerChooser; +use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; +use Symfony\Component\Serializer\Normalizer\UidNormalizer; +use Symfony\Component\Uid\Uuid; + +class NormalizerChooserTest extends TestCase +{ + public function testChooseNormalizerWithNoNormalizer() + { + $chooser = new NormalizerChooser(); + $this->assertNull($chooser->chooseNormalizer([], 'foo')); + } + + public function testChooseDenormalizerWithNoDenormalizer() + { + $chooser = new NormalizerChooser(); + $this->assertNull($chooser->chooseDenormalizer([], 'foo', \stdClass::class)); + } + + public function testChooseNormalizerWithPriority() + { + $chooser = new NormalizerChooser(); + + $normalizers = [ + $dateTimeNormalizer = new DateTimeNormalizer(), + $objectNormalizer = new ObjectNormalizer() + ]; + + $this->assertSame($dateTimeNormalizer, $chooser->chooseNormalizer($normalizers, new \DateTime())); + $this->assertSame($objectNormalizer, $chooser->chooseNormalizer($normalizers, new \stdClass())); + } + + public function testChooseDenormalizerWithPriority() + { + $chooser = new NormalizerChooser(); + + $denormalizers = [ + $dateTimeNormalizer = new DateTimeNormalizer(), + $uidNormalizer = new UidNormalizer() + ]; + + $this->assertSame($dateTimeNormalizer, $chooser->chooseNormalizer($denormalizers, new \DateTime())); + $this->assertSame($uidNormalizer, $chooser->chooseNormalizer($denormalizers, Uuid::v4())); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php index 7806be7cbb496..8a67e37fa3b00 100644 --- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php +++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php @@ -29,7 +29,9 @@ use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\Chooser\NormalizerChooserInterface; use Symfony\Component\Serializer\Normalizer\CustomNormalizer; +use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer; @@ -640,6 +642,18 @@ public function testDeserializeAndUnwrap() $serializer->deserialize($jsonData, __NAMESPACE__.'\Model', 'json', [UnwrappingDenormalizer::UNWRAP_PATH => '[baz][inner]']) ); } + + public function testUseCustomNormalizerChooser() + { + $chooser = $this->createMock(NormalizerChooserInterface::class); + $chooser->method('chooseNormalizer')->willReturn($normalizer = new DateTimeNormalizer()); + $chooser->method('chooseDenormalizer')->willReturn($normalizer); + + $serializer = new Serializer([$normalizer]); + $serializer->setNormalizerChooser($chooser); + $this->assertSame('1970-01-01T00:00:00+00:00', $serializer->normalize(new \DateTime('1970-01-01'))); + $this->assertEquals(new \DateTime('1970-01-01'), $serializer->denormalize('1970-01-01T00:00:00+00:00', \DateTime::class)); + } } class Model pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy