Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions UPGRADE-6.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----------
Expand Down
6 changes: 6 additions & 0 deletions src/Symfony/Component/Serializer/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
---

Expand Down
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;

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
Expand Up @@ -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
*/
interface CacheableSupportsMethodInterface
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = [] */);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [] */);
}
71 changes: 59 additions & 12 deletions src/Symfony/Component/Serializer/Serializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)) {
return $normalizer;
}
}

return null;
}

/**
* Backwards-Compatibility layer for CacheableSupportsMethodInterface -> CacheableSupport.
*/
private function supportsNormalizationWrapper(NormalizerInterface $normalizer, mixed $data, ?string $format, array $context): CacheableSupport
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can find a more meaningful name for that private method?
Or at least add a comment telling that is a BC layer?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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,
default => $value
};
}

/**
* Returns a matching denormalizer.
*
Expand All @@ -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);
Expand Down
24 changes: 24 additions & 0 deletions src/Symfony/Component/Serializer/Tests/SerializerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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]));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can test the SupportNever as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is testing that, by calling supports.. multiple times and ensuring supportsNormalization is only called 3 times instead of 5.

$normalizer
            ->expects($this->exactly(3))
            ->method('supportsNormalization')

Unless you'd say something more explicit could be added?

}

class Model
Expand Down