-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Serializer] Improve performance by exposing supports-never/-always #45779
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <[email protected]> | ||
* | ||
* 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 <https://github.com/Jeroeny> | ||
*/ | ||
enum CacheableSupport: int | ||
{ | ||
case SupportNever = -1; | ||
case SupportNot = 0; | ||
case Support = 1; | ||
case SupportAlways = 2; | ||
Jeroeny marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
public function supports(): bool | ||
{ | ||
return match ($this) { | ||
CacheableSupport::SupportNever, CacheableSupport::SupportNot => false, | ||
CacheableSupport::Support, CacheableSupport::SupportAlways => true, | ||
}; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,8 @@ | |
* supports*() methods will be cached by type and format. | ||
* | ||
* @author Kévin Dunglas <[email protected]> | ||
* | ||
* @deprecated since symfony/serializer 6.1, return CacheableSupport from the supports*() method instead | ||
Jeroeny marked this conversation as resolved.
Show resolved
Hide resolved
|
||
*/ | ||
interface CacheableSupportsMethodInterface | ||
{ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,34 +248,56 @@ 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; | ||
} | ||
} | ||
} | ||
|
||
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)) { | ||
Jeroeny marked this conversation as resolved.
Show resolved
Hide resolved
Jeroeny marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return $normalizer; | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
|
||
/** | ||
* Backwards-Compatibility layer for CacheableSupportsMethodInterface -> CacheableSupport. | ||
*/ | ||
private function supportsNormalizationWrapper(NormalizerInterface $normalizer, mixed $data, ?string $format, array $context): CacheableSupport | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we can find a more meaningful name for that private method? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a comment. The name could be better indeed, but haven't thought of anything better yet, am open to suggestions. |
||
{ | ||
$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, | ||
Jeroeny marked this conversation as resolved.
Show resolved
Hide resolved
|
||
default => $value | ||
}; | ||
} | ||
|
||
/** | ||
* Returns a matching denormalizer. | ||
* | ||
|
@@ -285,33 +308,57 @@ 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) { | ||
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; | ||
$support = $this->supportsDenormalizationWrapper($normalizer, $data, $class, $format, $context); | ||
if (CacheableSupport::SupportNever === $support) { | ||
continue; | ||
} | ||
|
||
$this->denormalizerCache[$format][$class][$k] = $support; | ||
if (CacheableSupport::SupportAlways === $support) { | ||
break; | ||
} | ||
} | ||
} | ||
|
||
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; | ||
} | ||
} | ||
|
||
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); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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])); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we can test the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is testing that, by calling
Unless you'd say something more explicit could be added? |
||
} | ||
|
||
class Model | ||
|
Uh oh!
There was an error while loading. Please reload this page.