diff --git a/UPGRADE-6.2.md b/UPGRADE-6.2.md index 3879850dc0abc..60a2a46318f51 100644 --- a/UPGRADE-6.2.md +++ b/UPGRADE-6.2.md @@ -102,6 +102,7 @@ Serializer * Deprecate calling `AttributeMetadata::setSerializedName()`, `ClassMetadata::setClassDiscriminatorMapping()` without arguments * Change the signature of `AttributeMetadataInterface::setSerializedName()` to `setSerializedName(?string)` * Change the signature of `ClassMetadataInterface::setClassDiscriminatorMapping()` to `setClassDiscriminatorMapping(?ClassDiscriminatorMapping)` + * Deprecate `Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface`, use `Symfony\Component\Serializer\Normalizer\CacheableSupport` instead Translation ----------- diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md index 3342ada2fea86..c26294080f222 100644 --- a/src/Symfony/Component/Serializer/CHANGELOG.md +++ b/src/Symfony/Component/Serializer/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +6.3 +--- + +* Cache normalizer selection based on format and type +* Deprecate `Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface`, use `Symfony\Component\Serializer\Normalizer\CacheableSupport` instead + 6.2 --- diff --git a/src/Symfony/Component/Serializer/Normalizer/CacheableSupport.php b/src/Symfony/Component/Serializer/Normalizer/CacheableSupport.php new file mode 100644 index 0000000000000..d1ee3d6d7f735 --- /dev/null +++ b/src/Symfony/Component/Serializer/Normalizer/CacheableSupport.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Normalizer; + +/** + * The return value of supports*() methods in {@see NormalizerInterface} and {@see DenormalizerInterface}. + * Tells if the result should be cached based on type and format. + * + * -1 : no, never supports the $format+$type, cache it + * 0 : no, no cache + * 1 : yes, no cache + * 2 : yes, always supports the $format+$type, cache it + * + * @author Jeroen Spee + */ +enum CacheableSupport: int +{ + case SupportNever = -1; + case SupportNot = 0; + case Support = 1; + case SupportAlways = 2; + + public function supports(): bool + { + return match ($this) { + CacheableSupport::SupportNever, CacheableSupport::SupportNot => false, + CacheableSupport::Support, CacheableSupport::SupportAlways => true, + }; + } +} diff --git a/src/Symfony/Component/Serializer/Normalizer/CacheableSupportsMethodInterface.php b/src/Symfony/Component/Serializer/Normalizer/CacheableSupportsMethodInterface.php index 3a55f653b1786..66e9b5ee27c48 100644 --- a/src/Symfony/Component/Serializer/Normalizer/CacheableSupportsMethodInterface.php +++ b/src/Symfony/Component/Serializer/Normalizer/CacheableSupportsMethodInterface.php @@ -19,6 +19,8 @@ * supports*() methods will be cached by type and format. * * @author Kévin Dunglas + * + * @deprecated since symfony/serializer 6.1, return CacheableSupport from the supports*() method instead */ interface CacheableSupportsMethodInterface { diff --git a/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php b/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php index ae3adbfe330fa..e225d435ffd45 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php +++ b/src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php @@ -34,8 +34,6 @@ interface DenormalizerInterface * @param string $format Format the given data was extracted from * @param array $context Options available to the denormalizer * - * @return mixed - * * @throws BadMethodCallException Occurs when the normalizer is not called in an expected context * @throws InvalidArgumentException Occurs when the arguments are not coherent or not supported * @throws UnexpectedValueException Occurs when the item cannot be hydrated with the given data @@ -49,12 +47,11 @@ public function denormalize(mixed $data, string $type, string $format = null, ar /** * Checks whether the given class is supported for denormalization by this normalizer. * - * @param mixed $data Data to denormalize from - * @param string $type The class to which the data should be denormalized - * @param string $format The format being deserialized from - * @param array $context Options available to the denormalizer + * @param mixed $data Data to denormalize from + * @param string $type The class to which the data should be denormalized + * @param string $format The format being deserialized from * - * @return bool + * @return CacheableSupport|bool returning a boolean is deprecated */ public function supportsDenormalization(mixed $data, string $type, string $format = null /* , array $context = [] */); } diff --git a/src/Symfony/Component/Serializer/Normalizer/NormalizerInterface.php b/src/Symfony/Component/Serializer/Normalizer/NormalizerInterface.php index 691e9c70f01cb..cd849cb80fca1 100644 --- a/src/Symfony/Component/Serializer/Normalizer/NormalizerInterface.php +++ b/src/Symfony/Component/Serializer/Normalizer/NormalizerInterface.php @@ -41,11 +41,10 @@ public function normalize(mixed $object, string $format = null, array $context = /** * Checks whether the given class is supported for normalization by this normalizer. * - * @param mixed $data Data to normalize - * @param string $format The format being (de-)serialized from or into - * @param array $context Context options for the normalizer + * @param mixed $data Data to normalize + * @param string $format The format being (de-)serialized from or into * - * @return bool + * @return CacheableSupport|bool returning a boolean is deprecated */ public function supportsNormalization(mixed $data, string $format = null /* , array $context = [] */); } diff --git a/src/Symfony/Component/Serializer/Serializer.php b/src/Symfony/Component/Serializer/Serializer.php index 85a3ac5b558d4..2713e5a391b59 100644 --- a/src/Symfony/Component/Serializer/Serializer.php +++ b/src/Symfony/Component/Serializer/Serializer.php @@ -23,6 +23,7 @@ use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Exception\PartialDenormalizationException; use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; +use Symfony\Component\Serializer\Normalizer\CacheableSupport; use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface; use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; @@ -247,19 +248,24 @@ public function supportsDenormalization(mixed $data, string $type, string $forma private function getNormalizer(mixed $data, ?string $format, array $context): ?NormalizerInterface { $type = \is_object($data) ? $data::class : 'native-'.\gettype($data); - + $minCached = CacheableSupport::SupportAlways; + $minUncached = CacheableSupport::SupportNever; if (!isset($this->normalizerCache[$format][$type])) { + $minCached = CacheableSupport::Support; + $minUncached = CacheableSupport::SupportNot; $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; + $support = $this->supportsNormalizationWrapper($normalizer, $data, $format, $context); + if (CacheableSupport::SupportNever === $support) { + continue; + } + + $this->normalizerCache[$format][$type][$k] = $support; + if (CacheableSupport::SupportAlways === $support) { break; } } @@ -267,7 +273,7 @@ private function getNormalizer(mixed $data, ?string $format, array $context): ?N foreach ($this->normalizerCache[$format][$type] as $k => $cached) { $normalizer = $this->normalizers[$k]; - if ($cached || $normalizer->supportsNormalization($data, $format, $context)) { + if ($cached->value >= $minCached->value || ($cached->value > $minUncached->value && $this->supportsNormalizationWrapper($normalizer, $data, $format, $context)->value > CacheableSupport::SupportNot->value)) { return $normalizer; } } @@ -275,6 +281,23 @@ private function getNormalizer(mixed $data, ?string $format, array $context): ?N return null; } + /** + * Backwards-Compatibility layer for CacheableSupportsMethodInterface -> CacheableSupport. + */ + private function supportsNormalizationWrapper(NormalizerInterface $normalizer, mixed $data, ?string $format, array $context): CacheableSupport + { + $value = $normalizer->supportsNormalization($data, $format, $context); + if (\is_bool($value)) { + trigger_deprecation('symfony/serializer', '6.2', 'Returning boolean from "%s::%s" is deprecated, return "%s" instead.', NormalizerInterface::class, 'supports()', CacheableSupport::class); + } + + return match ($value) { + true => $normalizer instanceof CacheableSupportsMethodInterface && $normalizer->hasCacheableSupportsMethod() ? CacheableSupport::SupportAlways : CacheableSupport::Support, + false => $normalizer instanceof CacheableSupportsMethodInterface && $normalizer->hasCacheableSupportsMethod() ? CacheableSupport::SupportNever : CacheableSupport::SupportNot, + default => $value + }; + } + /** * Returns a matching denormalizer. * @@ -285,7 +308,11 @@ private function getNormalizer(mixed $data, ?string $format, array $context): ?N */ private function getDenormalizer(mixed $data, string $class, ?string $format, array $context): ?DenormalizerInterface { + $minCached = CacheableSupport::SupportAlways; + $minUncached = CacheableSupport::SupportNever; if (!isset($this->denormalizerCache[$format][$class])) { + $minCached = CacheableSupport::Support; + $minUncached = CacheableSupport::SupportNot; $this->denormalizerCache[$format][$class] = []; foreach ($this->normalizers as $k => $normalizer) { @@ -293,10 +320,13 @@ private function getDenormalizer(mixed $data, string $class, ?string $format, ar 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; + $support = $this->supportsDenormalizationWrapper($normalizer, $data, $class, $format, $context); + if (CacheableSupport::SupportNever === $support) { + continue; + } + + $this->denormalizerCache[$format][$class][$k] = $support; + if (CacheableSupport::SupportAlways === $support) { break; } } @@ -304,7 +334,7 @@ private function getDenormalizer(mixed $data, string $class, ?string $format, ar foreach ($this->denormalizerCache[$format][$class] as $k => $cached) { $normalizer = $this->normalizers[$k]; - if ($cached || $normalizer->supportsDenormalization($data, $class, $format, $context)) { + if ($cached->value >= $minCached->value || ($cached->value > $minUncached->value && $this->supportsDenormalizationWrapper($normalizer, $data, $class, $format, $context)->value > CacheableSupport::SupportNot->value)) { return $normalizer; } } @@ -312,6 +342,23 @@ private function getDenormalizer(mixed $data, string $class, ?string $format, ar return null; } + /** + * Backwards-Compatibility layer for CacheableSupportsMethodInterface -> CacheableSupport. + */ + private function supportsDenormalizationWrapper(DenormalizerInterface $normalizer, mixed $data, string $class, ?string $format, array $context): CacheableSupport + { + $value = $normalizer->supportsDenormalization($data, $class, $format, $context); + if (\is_bool($value)) { + trigger_deprecation('symfony/serializer', '6.2', 'Returning boolean from "%s::%s" is deprecated, return "%s" instead.', DenormalizerInterface::class, 'supports()', CacheableSupport::class); + } + + return match ($value) { + true => $normalizer instanceof CacheableSupportsMethodInterface && $normalizer->hasCacheableSupportsMethod() ? CacheableSupport::SupportAlways : CacheableSupport::Support, + false => $normalizer instanceof CacheableSupportsMethodInterface && $normalizer->hasCacheableSupportsMethod() ? CacheableSupport::SupportNever : CacheableSupport::SupportNot, + default => $value + }; + } + final public function encode(mixed $data, string $format, array $context = []): string { return $this->encoder->encode($data, $format, $context); diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php index 255abb3864b1d..dd79b8eae51ec 100644 --- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php +++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php @@ -37,6 +37,7 @@ use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer; +use Symfony\Component\Serializer\Normalizer\CacheableSupport; use Symfony\Component\Serializer\Normalizer\CustomNormalizer; use Symfony\Component\Serializer\Normalizer\DataUriNormalizer; use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; @@ -1247,6 +1248,29 @@ public function provideCollectDenormalizationErrors() [new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()))], ]; } + + public function testCacheableNormalizer() + { + $normalizer = $this->createMock(NormalizerInterface::class); + $serializer = new Serializer([$normalizer], []); + + $normalizer + ->expects($this->exactly(3)) + ->method('supportsNormalization') + ->willReturnCallback(function ($data, $format, array $context = []): CacheableSupport { + if (!$data instanceof Bar) { + return CacheableSupport::SupportNever; + } + + return ($context['TEST_CONTEXT'] ?? false) ? CacheableSupport::Support : CacheableSupport::SupportNot; + }); + + $this->assertTrue($serializer->supportsNormalization(new Bar(''), 'json', ['TEST_CONTEXT' => true])); + $this->assertFalse($serializer->supportsNormalization(new Bar(''), 'json')); + $this->assertFalse($serializer->supportsNormalization(new \stdClass(), 'json', ['TEST_CONTEXT' => true])); + $this->assertFalse($serializer->supportsNormalization(new \stdClass(), 'json', ['TEST_CONTEXT' => true])); + $this->assertFalse($serializer->supportsNormalization(new \stdClass(), 'json', ['TEST_CONTEXT' => true])); + } } class Model