Upgrade 1-11.38

This commit is contained in:
xesmyd
2026-03-30 14:10:30 +02:00
parent f2a7e6d1fc
commit ac648ef29d
24665 changed files with 69682 additions and 2205004 deletions
+244 -73
View File
@@ -14,34 +14,123 @@ namespace Symfony\Component\Serializer\Normalizer;
use Symfony\Component\Serializer\Exception\CircularReferenceException;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
use Symfony\Component\Serializer\Exception\RuntimeException;
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerAwareTrait;
/**
* Normalizer implementation.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
abstract class AbstractNormalizer extends SerializerAwareNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface, CacheableSupportsMethodInterface
{
use ObjectToPopulateTrait;
use SerializerAwareTrait;
const CIRCULAR_REFERENCE_LIMIT = 'circular_reference_limit';
const OBJECT_TO_POPULATE = 'object_to_populate';
const GROUPS = 'groups';
const ATTRIBUTES = 'attributes';
const ALLOW_EXTRA_ATTRIBUTES = 'allow_extra_attributes';
/* constants to configure the context */
/**
* @var int
* How many loops of circular reference to allow while normalizing.
*
* The default value of 1 means that when we encounter the same object a
* second time, we consider that a circular reference.
*
* You can raise this value for special cases, e.g. in combination with the
* max depth setting of the object normalizer.
*/
public const CIRCULAR_REFERENCE_LIMIT = 'circular_reference_limit';
/**
* Instead of creating a new instance of an object, update the specified object.
*
* If you have a nested structure, child objects will be overwritten with
* new instances unless you set DEEP_OBJECT_TO_POPULATE to true.
*/
public const OBJECT_TO_POPULATE = 'object_to_populate';
/**
* Only (de)normalize attributes that are in the specified groups.
*/
public const GROUPS = 'groups';
/**
* Limit (de)normalize to the specified names.
*
* For nested structures, this list needs to reflect the object tree.
*/
public const ATTRIBUTES = 'attributes';
/**
* If ATTRIBUTES are specified, and the source has fields that are not part of that list,
* either ignore those attributes (true) or throw an ExtraAttributesException (false).
*/
public const ALLOW_EXTRA_ATTRIBUTES = 'allow_extra_attributes';
/**
* Hashmap of default values for constructor arguments.
*
* The names need to match the parameter names in the constructor arguments.
*/
public const DEFAULT_CONSTRUCTOR_ARGUMENTS = 'default_constructor_arguments';
/**
* Hashmap of field name => callable to (de)normalize this field.
*
* The callable is called if the field is encountered with the arguments:
*
* - mixed $attributeValue value of this field
* - object|string $object the whole object being normalized or the object's class being denormalized
* - string $attributeName name of the attribute being (de)normalized
* - string $format the requested format
* - array $context the serialization context
*/
public const CALLBACKS = 'callbacks';
/**
* Handler to call when a circular reference has been detected.
*
* If you specify no handler, a CircularReferenceException is thrown.
*
* The method will be called with ($object, $format, $context) and its
* return value is returned as the result of the normalize call.
*/
public const CIRCULAR_REFERENCE_HANDLER = 'circular_reference_handler';
/**
* Skip the specified attributes when normalizing an object tree.
*
* This list is applied to each element of nested structures.
*
* Note: The behaviour for nested structures is different from ATTRIBUTES
* for historical reason. Aligning the behaviour would be a BC break.
*/
public const IGNORED_ATTRIBUTES = 'ignored_attributes';
/**
* @internal
*/
protected const CIRCULAR_REFERENCE_LIMIT_COUNTERS = 'circular_reference_limit_counters';
protected $defaultContext = [
self::ALLOW_EXTRA_ATTRIBUTES => true,
self::CIRCULAR_REFERENCE_LIMIT => 1,
self::IGNORED_ATTRIBUTES => [],
];
/**
* @deprecated since Symfony 4.2
*/
protected $circularReferenceLimit = 1;
/**
* @var callable
* @deprecated since Symfony 4.2
*
* @var callable|null
*/
protected $circularReferenceHandler;
@@ -56,31 +145,40 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
protected $nameConverter;
/**
* @var array
* @deprecated since Symfony 4.2
*/
protected $callbacks = [];
/**
* @var array
* @deprecated since Symfony 4.2
*/
protected $ignoredAttributes = [];
/**
* @var array
* @deprecated since Symfony 4.2
*/
protected $camelizedAttributes = [];
/**
* Sets the {@link ClassMetadataFactoryInterface} to use.
*/
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null)
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, array $defaultContext = [])
{
$this->classMetadataFactory = $classMetadataFactory;
$this->nameConverter = $nameConverter;
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
$this->validateCallbackContext($this->defaultContext, 'default');
if (isset($this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER]) && !\is_callable($this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER])) {
throw new InvalidArgumentException(sprintf('Invalid callback found in the "%s" default context option.', self::CIRCULAR_REFERENCE_HANDLER));
}
}
/**
* Set circular reference limit.
* Sets circular reference limit.
*
* @deprecated since Symfony 4.2
*
* @param int $circularReferenceLimit Limit of iterations for the same object
*
@@ -88,27 +186,35 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
*/
public function setCircularReferenceLimit($circularReferenceLimit)
{
$this->circularReferenceLimit = $circularReferenceLimit;
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the "circular_reference_limit" key of the context instead.', __METHOD__), \E_USER_DEPRECATED);
$this->defaultContext[self::CIRCULAR_REFERENCE_LIMIT] = $this->circularReferenceLimit = $circularReferenceLimit;
return $this;
}
/**
* Set circular reference handler.
* Sets circular reference handler.
*
* @deprecated since Symfony 4.2
*
* @return self
*/
public function setCircularReferenceHandler(callable $circularReferenceHandler)
{
$this->circularReferenceHandler = $circularReferenceHandler;
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the "circular_reference_handler" key of the context instead.', __METHOD__), \E_USER_DEPRECATED);
$this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER] = $this->circularReferenceHandler = $circularReferenceHandler;
return $this;
}
/**
* Set normalization callbacks.
* Sets (de)normalization callbacks.
*
* @param callable[] $callbacks Help normalize the result
* @deprecated since Symfony 4.2
*
* @param callable[] $callbacks Help (de)normalize the result
*
* @return self
*
@@ -116,28 +222,42 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
*/
public function setCallbacks(array $callbacks)
{
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the "callbacks" key of the context instead.', __METHOD__), \E_USER_DEPRECATED);
foreach ($callbacks as $attribute => $callback) {
if (!\is_callable($callback)) {
throw new InvalidArgumentException(sprintf('The given callback for attribute "%s" is not callable.', $attribute));
}
}
$this->callbacks = $callbacks;
$this->defaultContext[self::CALLBACKS] = $this->callbacks = $callbacks;
return $this;
}
/**
* Set ignored attributes for normalization and denormalization.
* Sets ignored attributes for normalization and denormalization.
*
* @deprecated since Symfony 4.2
*
* @return self
*/
public function setIgnoredAttributes(array $ignoredAttributes)
{
$this->ignoredAttributes = $ignoredAttributes;
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the "ignored_attributes" key of the context instead.', __METHOD__), \E_USER_DEPRECATED);
$this->defaultContext[self::IGNORED_ATTRIBUTES] = $this->ignoredAttributes = $ignoredAttributes;
return $this;
}
/**
* {@inheritdoc}
*/
public function hasCacheableSupportsMethod(): bool
{
return false;
}
/**
* Detects if the configured circular reference limit is reached.
*
@@ -152,16 +272,17 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
{
$objectHash = spl_object_hash($object);
if (isset($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash])) {
if ($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash] >= $this->circularReferenceLimit) {
unset($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash]);
$circularReferenceLimit = $context[self::CIRCULAR_REFERENCE_LIMIT] ?? $this->defaultContext[self::CIRCULAR_REFERENCE_LIMIT] ?? $this->circularReferenceLimit;
if (isset($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash])) {
if ($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash] >= $circularReferenceLimit) {
unset($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash]);
return true;
}
++$context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash];
++$context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash];
} else {
$context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash] = 1;
$context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash] = 1;
}
return false;
@@ -173,19 +294,30 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
* If a circular reference handler is set, it will be called. Otherwise, a
* {@class CircularReferenceException} will be thrown.
*
* @param object $object
* @final since Symfony 4.2
*
* @param object $object
* @param string|null $format
* @param array $context
*
* @return mixed
*
* @throws CircularReferenceException
*/
protected function handleCircularReference($object)
protected function handleCircularReference($object/* , string $format = null, array $context = [] */)
{
if ($this->circularReferenceHandler) {
return \call_user_func($this->circularReferenceHandler, $object);
if (\func_num_args() < 2 && __CLASS__ !== static::class && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface && !$this instanceof \Mockery\MockInterface) {
@trigger_error(sprintf('The "%s()" method will have two new "string $format = null" and "array $context = []" arguments in version 5.0, not defining it is deprecated since Symfony 4.2.', __METHOD__), \E_USER_DEPRECATED);
}
$format = \func_num_args() > 1 ? func_get_arg(1) : null;
$context = \func_num_args() > 2 ? func_get_arg(2) : [];
$circularReferenceHandler = $context[self::CIRCULAR_REFERENCE_HANDLER] ?? $this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER] ?? $this->circularReferenceHandler;
if ($circularReferenceHandler) {
return $circularReferenceHandler($object, $format, $context);
}
throw new CircularReferenceException(sprintf('A circular reference has been detected when serializing the object of class "%s" (configured limit: %d).', \get_class($object), $this->circularReferenceLimit));
throw new CircularReferenceException(sprintf('A circular reference has been detected when serializing the object of class "%s" (configured limit: %d).', \get_class($object), $context[self::CIRCULAR_REFERENCE_LIMIT] ?? $this->defaultContext[self::CIRCULAR_REFERENCE_LIMIT] ?? $this->circularReferenceLimit));
}
/**
@@ -200,18 +332,18 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
*/
protected function getAllowedAttributes($classOrObject, array $context, $attributesAsString = false)
{
$allowExtraAttributes = $context[self::ALLOW_EXTRA_ATTRIBUTES] ?? $this->defaultContext[self::ALLOW_EXTRA_ATTRIBUTES];
if (!$this->classMetadataFactory) {
if (isset($context[static::ALLOW_EXTRA_ATTRIBUTES]) && !$context[static::ALLOW_EXTRA_ATTRIBUTES]) {
throw new LogicException(sprintf('A class metadata factory must be provided in the constructor when setting "%s" to false.', static::ALLOW_EXTRA_ATTRIBUTES));
if (!$allowExtraAttributes) {
throw new LogicException(sprintf('A class metadata factory must be provided in the constructor when setting "%s" to false.', self::ALLOW_EXTRA_ATTRIBUTES));
}
return false;
}
$groups = false;
if (isset($context[static::GROUPS]) && \is_array($context[static::GROUPS])) {
$groups = $context[static::GROUPS];
} elseif (!isset($context[static::ALLOW_EXTRA_ATTRIBUTES]) || $context[static::ALLOW_EXTRA_ATTRIBUTES]) {
$tmpGroups = $context[self::GROUPS] ?? $this->defaultContext[self::GROUPS] ?? null;
$groups = (\is_array($tmpGroups) || \is_scalar($tmpGroups)) ? (array) $tmpGroups : false;
if (false === $groups && $allowExtraAttributes) {
return false;
}
@@ -241,17 +373,19 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
*/
protected function isAllowedAttribute($classOrObject, $attribute, $format = null, array $context = [])
{
if (\in_array($attribute, $this->ignoredAttributes)) {
$ignoredAttributes = $context[self::IGNORED_ATTRIBUTES] ?? $this->defaultContext[self::IGNORED_ATTRIBUTES] ?? $this->ignoredAttributes;
if (\in_array($attribute, $ignoredAttributes)) {
return false;
}
if (isset($context[self::ATTRIBUTES][$attribute])) {
$attributes = $context[self::ATTRIBUTES] ?? $this->defaultContext[self::ATTRIBUTES] ?? null;
if (isset($attributes[$attribute])) {
// Nested attributes
return true;
}
if (isset($context[self::ATTRIBUTES]) && \is_array($context[self::ATTRIBUTES])) {
return \in_array($attribute, $context[self::ATTRIBUTES], true);
if (\is_array($attributes)) {
return \in_array($attribute, $attributes, true);
}
return true;
@@ -298,24 +432,12 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
* @return object
*
* @throws RuntimeException
* @throws MissingConstructorArgumentsException
*/
protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes/*, string $format = null*/)
protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes, string $format = null)
{
if (\func_num_args() >= 6) {
$format = func_get_arg(5);
} else {
if (__CLASS__ !== static::class) {
$r = new \ReflectionMethod($this, __FUNCTION__);
if (__CLASS__ !== $r->getDeclaringClass()->getName()) {
@trigger_error(sprintf('Method %s::%s() will have a 6th `string $format = null` argument in version 4.0. Not defining it is deprecated since Symfony 3.2.', static::class, __FUNCTION__), \E_USER_DEPRECATED);
}
}
$format = null;
}
if (null !== $object = $this->extractObjectToPopulate($class, $context, static::OBJECT_TO_POPULATE)) {
unset($context[static::OBJECT_TO_POPULATE]);
if (null !== $object = $this->extractObjectToPopulate($class, $context, self::OBJECT_TO_POPULATE)) {
unset($context[self::OBJECT_TO_POPULATE]);
return $object;
}
@@ -333,11 +455,11 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
$params = [];
foreach ($constructorParameters as $constructorParameter) {
$paramName = $constructorParameter->name;
$key = $this->nameConverter ? $this->nameConverter->normalize($paramName) : $paramName;
$key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName;
$allowed = false === $allowedAttributes || \in_array($paramName, $allowedAttributes);
$ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context);
if (method_exists($constructorParameter, 'isVariadic') && $constructorParameter->isVariadic()) {
if ($constructorParameter->isVariadic()) {
if ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
if (!\is_array($data[$paramName])) {
throw new RuntimeException(sprintf('Cannot create an instance of "%s" from serialized data because the variadic parameter "%s" can only accept an array.', $class, $constructorParameter->name));
@@ -363,10 +485,16 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
// Don't run set for a parameter passed to the constructor
$params[] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $context, $format);
unset($data[$key]);
} elseif (\array_key_exists($key, $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class] ?? [])) {
$params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
} elseif (\array_key_exists($key, $this->defaultContext[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class] ?? [])) {
$params[] = $this->defaultContext[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
} elseif ($constructorParameter->isDefaultValueAvailable()) {
$params[] = $constructorParameter->getDefaultValue();
} elseif ($constructorParameter->hasType() && $constructorParameter->getType()->allowsNull()) {
$params[] = null;
} else {
throw new RuntimeException(sprintf('Cannot create an instance of "%s" from serialized data because its constructor requires parameter "%s" to be present.', $class, $constructorParameter->name));
throw new MissingConstructorArgumentsException(sprintf('Cannot create an instance of "%s" from serialized data because its constructor requires parameter "%s" to be present.', $class, $constructorParameter->name));
}
}
@@ -386,38 +514,39 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
protected function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameter, $parameterName, $parameterData, array $context, $format = null)
{
try {
if (\PHP_VERSION_ID < 70100 && null !== $parameterClass = $parameter->getClass()) {
$parameterClass = $parameterClass->name;
} elseif (\PHP_VERSION_ID >= 70100 && ($parameterType = $parameter->getType()) instanceof \ReflectionNamedType && !$parameterType->isBuiltin()) {
if (($parameterType = $parameter->getType()) instanceof \ReflectionNamedType && !$parameterType->isBuiltin()) {
$parameterClass = $parameterType->getName();
new \ReflectionClass($parameterClass); // throws a \ReflectionException if the class doesn't exist
} else {
$parameterClass = null;
}
if (null !== $parameterClass) {
if (!$this->serializer instanceof DenormalizerInterface) {
throw new LogicException(sprintf('Cannot create an instance of "%s" from serialized data because the serializer inject in "%s" is not a denormalizer.', $parameterClass, static::class));
}
return $this->serializer->denormalize($parameterData, $parameterClass, $format, $this->createChildContext($context, $parameterName, $format));
$parameterData = $this->serializer->denormalize($parameterData, $parameterClass, $format, $this->createChildContext($context, $parameterName, $format));
}
return $parameterData;
} catch (\ReflectionException $e) {
throw new RuntimeException(sprintf('Could not determine the class of the parameter "%s".', $parameterName), 0, $e);
} catch (MissingConstructorArgumentsException $e) {
if (!$parameter->getType()->allowsNull()) {
throw $e;
}
return null;
}
return $this->applyCallbacks($parameterData, $class->getName(), $parameterName, $format, $context);
}
/**
* @param string $attribute Attribute name
*
* @return array
*
* @internal
*/
protected function createChildContext(array $parentContext, $attribute/*, string $format = null */)
protected function createChildContext(array $parentContext, $attribute/* , ?string $format */): array
{
if (\func_num_args() < 3) {
@trigger_error(sprintf('Method "%s::%s()" will have a third "?string $format" argument in version 5.0; not defining it is deprecated since Symfony 4.3.', static::class, __FUNCTION__), \E_USER_DEPRECATED);
}
if (isset($parentContext[self::ATTRIBUTES][$attribute])) {
$parentContext[self::ATTRIBUTES] = $parentContext[self::ATTRIBUTES][$attribute];
} else {
@@ -426,4 +555,46 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
return $parentContext;
}
/**
* Validate callbacks set in context.
*
* @param string $contextType Used to specify which context is invalid in exceptions
*
* @throws InvalidArgumentException
*/
final protected function validateCallbackContext(array $context, string $contextType = ''): void
{
if (!isset($context[self::CALLBACKS])) {
return;
}
if (!\is_array($context[self::CALLBACKS])) {
throw new InvalidArgumentException(sprintf('The "%s"%s context option must be an array of callables.', self::CALLBACKS, '' !== $contextType ? " $contextType" : ''));
}
foreach ($context[self::CALLBACKS] as $attribute => $callback) {
if (!\is_callable($callback)) {
throw new InvalidArgumentException(sprintf('Invalid callback found for attribute "%s" in the "%s"%s context option.', $attribute, self::CALLBACKS, '' !== $contextType ? " $contextType" : ''));
}
}
}
/**
* Apply callbacks set in context.
*
* @param mixed $value
* @param object|string $object Can be either the object being normalizing or the object's class being denormalized
*
* @return mixed
*/
final protected function applyCallbacks($value, $object, string $attribute, ?string $format, array $context)
{
/**
* @var callable|null
*/
$callback = $context[self::CALLBACKS][$attribute] ?? $this->defaultContext[self::CALLBACKS][$attribute] ?? $this->callbacks[$attribute] ?? null;
return $callback ? $callback($value, $object, $attribute, $format, $context) : $value;
}
}
@@ -11,14 +11,21 @@
namespace Symfony\Component\Serializer\Normalizer;
use Symfony\Component\PropertyAccess\Exception\AccessException;
use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException;
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Exception\ExtraAttributesException;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
use Symfony\Component\Serializer\Exception\RuntimeException;
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
@@ -29,19 +36,97 @@ use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
*/
abstract class AbstractObjectNormalizer extends AbstractNormalizer
{
const ENABLE_MAX_DEPTH = 'enable_max_depth';
const DEPTH_KEY_PATTERN = 'depth_%s::%s';
const DISABLE_TYPE_ENFORCEMENT = 'disable_type_enforcement';
/**
* Set to true to respect the max depth metadata on fields.
*/
public const ENABLE_MAX_DEPTH = 'enable_max_depth';
/**
* How to track the current depth in the context.
*/
public const DEPTH_KEY_PATTERN = 'depth_%s::%s';
/**
* While denormalizing, we can verify that types match.
*
* You can disable this by setting this flag to true.
*/
public const DISABLE_TYPE_ENFORCEMENT = 'disable_type_enforcement';
/**
* Flag to control whether fields with the value `null` should be output
* when normalizing or omitted.
*/
public const SKIP_NULL_VALUES = 'skip_null_values';
/**
* Callback to allow to set a value for an attribute when the max depth has
* been reached.
*
* If no callback is given, the attribute is skipped. If a callable is
* given, its return value is used (even if null).
*
* The arguments are:
*
* - mixed $attributeValue value of this field
* - object $object the whole object being normalized
* - string $attributeName name of the attribute being normalized
* - string $format the requested format
* - array $context the serialization context
*/
public const MAX_DEPTH_HANDLER = 'max_depth_handler';
/**
* Specify which context key are not relevant to determine which attributes
* of an object to (de)normalize.
*/
public const EXCLUDE_FROM_CACHE_KEY = 'exclude_from_cache_key';
/**
* Flag to tell the denormalizer to also populate existing objects on
* attributes of the main object.
*
* Setting this to true is only useful if you also specify the root object
* in OBJECT_TO_POPULATE.
*/
public const DEEP_OBJECT_TO_POPULATE = 'deep_object_to_populate';
public const PRESERVE_EMPTY_OBJECTS = 'preserve_empty_objects';
private $propertyTypeExtractor;
private $typesCache = [];
private $attributesCache = [];
private $cache = [];
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null)
/**
* @deprecated since Symfony 4.2
*
* @var callable|null
*/
private $maxDepthHandler;
private $objectClassResolver;
/**
* @var ClassDiscriminatorResolverInterface|null
*/
protected $classDiscriminatorResolver;
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = [])
{
parent::__construct($classMetadataFactory, $nameConverter);
parent::__construct($classMetadataFactory, $nameConverter, $defaultContext);
if (isset($this->defaultContext[self::MAX_DEPTH_HANDLER]) && !\is_callable($this->defaultContext[self::MAX_DEPTH_HANDLER])) {
throw new InvalidArgumentException(sprintf('The "%s" given in the default context is not callable.', self::MAX_DEPTH_HANDLER));
}
$this->defaultContext[self::EXCLUDE_FROM_CACHE_KEY] = array_merge($this->defaultContext[self::EXCLUDE_FROM_CACHE_KEY] ?? [], [self::CIRCULAR_REFERENCE_LIMIT_COUNTERS]);
$this->propertyTypeExtractor = $propertyTypeExtractor;
if (null === $classDiscriminatorResolver && null !== $classMetadataFactory) {
$classDiscriminatorResolver = new ClassDiscriminatorFromClassMetadata($classMetadataFactory);
}
$this->classDiscriminatorResolver = $classDiscriminatorResolver;
$this->objectClassResolver = $objectClassResolver;
}
/**
@@ -61,32 +146,61 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
$context['cache_key'] = $this->getCacheKey($format, $context);
}
$this->validateCallbackContext($context);
if ($this->isCircularReference($object, $context)) {
return $this->handleCircularReference($object);
return $this->handleCircularReference($object, $format, $context);
}
$data = [];
$stack = [];
$attributes = $this->getAttributes($object, $format, $context);
$class = \get_class($object);
$class = $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
$attributesMetadata = $this->classMetadataFactory ? $this->classMetadataFactory->getMetadataFor($class)->getAttributesMetadata() : null;
if (isset($context[self::MAX_DEPTH_HANDLER])) {
$maxDepthHandler = $context[self::MAX_DEPTH_HANDLER];
if (!\is_callable($maxDepthHandler)) {
throw new InvalidArgumentException(sprintf('The "%s" given in the context is not callable.', self::MAX_DEPTH_HANDLER));
}
} else {
// already validated in constructor resp by type declaration of setMaxDepthHandler
$maxDepthHandler = $this->defaultContext[self::MAX_DEPTH_HANDLER] ?? $this->maxDepthHandler;
}
foreach ($attributes as $attribute) {
if (null !== $attributesMetadata && $this->isMaxDepthReached($attributesMetadata, $class, $attribute, $context)) {
$maxDepthReached = false;
if (null !== $attributesMetadata && ($maxDepthReached = $this->isMaxDepthReached($attributesMetadata, $class, $attribute, $context)) && !$maxDepthHandler) {
continue;
}
$attributeValue = $this->getAttributeValue($object, $attribute, $format, $context);
if (isset($this->callbacks[$attribute])) {
$attributeValue = \call_user_func($this->callbacks[$attribute], $attributeValue);
try {
$attributeValue = $this->getAttributeValue($object, $attribute, $format, $context);
} catch (AccessException $e) {
if (sprintf('The property "%s::$%s" is not initialized.', \get_class($object), $attribute) === $e->getMessage()) {
continue;
}
if (($p = $e->getPrevious()) && 'Error' === \get_class($p) && $this->isUninitializedValueError($p)) {
continue;
}
throw $e;
} catch (\Error $e) {
if ($this->isUninitializedValueError($e)) {
continue;
}
throw $e;
}
if (null !== $attributeValue && !is_scalar($attributeValue)) {
if ($maxDepthReached) {
$attributeValue = $maxDepthHandler($attributeValue, $object, $attribute, $format, $context);
}
$attributeValue = $this->applyCallbacks($attributeValue, $object, $attribute, $format, $context);
if (null !== $attributeValue && !\is_scalar($attributeValue)) {
$stack[$attribute] = $attributeValue;
}
$data = $this->updateData($data, $attribute, $attributeValue);
$data = $this->updateData($data, $attribute, $attributeValue, $class, $format, $context);
}
foreach ($stack as $attribute => $attributeValue) {
@@ -94,12 +208,39 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
throw new LogicException(sprintf('Cannot normalize attribute "%s" because the injected serializer is not a normalizer.', $attribute));
}
$data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $this->createChildContext($context, $attribute, $format)));
$data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $this->createChildContext($context, $attribute, $format)), $class, $format, $context);
}
if (isset($context[self::PRESERVE_EMPTY_OBJECTS]) && !\count($data)) {
return new \ArrayObject();
}
return $data;
}
/**
* {@inheritdoc}
*/
protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes, string $format = null)
{
if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForClass($class)) {
if (!isset($data[$mapping->getTypeProperty()])) {
throw new RuntimeException(sprintf('Type property "%s" not found for the abstract object "%s".', $mapping->getTypeProperty(), $class));
}
$type = $data[$mapping->getTypeProperty()];
if (null === ($mappedClass = $mapping->getClassForType($type))) {
throw new RuntimeException(sprintf('The type "%s" has no mapped class for the abstract object "%s".', $type, $class));
}
if ($mappedClass !== $class) {
return $this->instantiateObject($data, $mappedClass, $context, new \ReflectionClass($mappedClass), $allowedAttributes, $format);
}
}
return parent::instantiateObject($data, $class, $context, $reflectionClass, $allowedAttributes, $format);
}
/**
* Gets and caches attributes for the given object, format and context.
*
@@ -110,7 +251,7 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
*/
protected function getAttributes($object, $format, array $context)
{
$class = \get_class($object);
$class = $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
$key = $class.'-'.$context['cache_key'];
if (isset($this->attributesCache[$key])) {
@@ -129,6 +270,10 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
$attributes = $this->extractAttributes($object, $format, $context);
if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForMappedObject($object)) {
array_unshift($attributes, $mapping->getTypeProperty());
}
if ($context['cache_key']) {
$this->attributesCache[$key] = $attributes;
}
@@ -157,12 +302,24 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
*/
abstract protected function getAttributeValue($object, $attribute, $format = null, array $context = []);
/**
* Sets a handler function that will be called when the max depth is reached.
*
* @deprecated since Symfony 4.2
*/
public function setMaxDepthHandler(?callable $handler): void
{
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the "max_depth_handler" key of the context instead.', __METHOD__), \E_USER_DEPRECATED);
$this->maxDepthHandler = $handler;
}
/**
* {@inheritdoc}
*/
public function supportsDenormalization($data, $type, $format = null)
{
return isset($this->cache[$type]) ? $this->cache[$type] : $this->cache[$type] = class_exists($type);
return class_exists($type) || (interface_exists($type, false) && $this->classDiscriminatorResolver && null !== $this->classDiscriminatorResolver->getMappingForClass($type));
}
/**
@@ -174,35 +331,47 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
$context['cache_key'] = $this->getCacheKey($format, $context);
}
$this->validateCallbackContext($context);
$allowedAttributes = $this->getAllowedAttributes($type, $context, true);
$normalizedData = $this->prepareForDenormalization($data);
$extraAttributes = [];
$reflectionClass = new \ReflectionClass($type);
$object = $this->instantiateObject($normalizedData, $type, $context, $reflectionClass, $allowedAttributes, $format);
$resolvedClass = $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
foreach ($normalizedData as $attribute => $value) {
if ($this->nameConverter) {
$attribute = $this->nameConverter->denormalize($attribute);
$attribute = $this->nameConverter->denormalize($attribute, $resolvedClass, $format, $context);
}
if ((false !== $allowedAttributes && !\in_array($attribute, $allowedAttributes)) || !$this->isAllowedAttribute($type, $attribute, $format, $context)) {
if (isset($context[self::ALLOW_EXTRA_ATTRIBUTES]) && !$context[self::ALLOW_EXTRA_ATTRIBUTES]) {
if ((false !== $allowedAttributes && !\in_array($attribute, $allowedAttributes)) || !$this->isAllowedAttribute($resolvedClass, $attribute, $format, $context)) {
if (!($context[self::ALLOW_EXTRA_ATTRIBUTES] ?? $this->defaultContext[self::ALLOW_EXTRA_ATTRIBUTES])) {
$extraAttributes[] = $attribute;
}
continue;
}
$value = $this->validateAndDenormalize($type, $attribute, $value, $format, $context);
if ($context[self::DEEP_OBJECT_TO_POPULATE] ?? $this->defaultContext[self::DEEP_OBJECT_TO_POPULATE] ?? false) {
try {
$context[self::OBJECT_TO_POPULATE] = $this->getAttributeValue($object, $attribute, $format, $context);
} catch (NoSuchPropertyException $e) {
}
}
$value = $this->validateAndDenormalize($resolvedClass, $attribute, $value, $format, $context);
$value = $this->applyCallbacks($value, $resolvedClass, $attribute, $format, $context);
try {
$this->setAttributeValue($object, $attribute, $value, $format, $context);
} catch (InvalidArgumentException $e) {
throw new NotNormalizableValueException($e->getMessage(), $e->getCode(), $e);
throw new NotNormalizableValueException(sprintf('Failed to denormalize attribute "%s" value for class "%s": '.$e->getMessage(), $attribute, $type), $e->getCode(), $e);
}
}
if (!empty($extraAttributes)) {
if ($extraAttributes) {
throw new ExtraAttributesException($extraAttributes);
}
@@ -222,23 +391,25 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
/**
* Validates the submitted data and denormalizes it.
*
* @param string $currentClass
* @param string $attribute
* @param mixed $data
* @param string|null $format
* @param mixed $data
*
* @return mixed
*
* @throws NotNormalizableValueException
* @throws ExtraAttributesException
* @throws MissingConstructorArgumentsException
* @throws LogicException
*/
private function validateAndDenormalize($currentClass, $attribute, $data, $format, array $context)
private function validateAndDenormalize(string $currentClass, string $attribute, $data, ?string $format, array $context)
{
if (null === $this->propertyTypeExtractor || null === $types = $this->propertyTypeExtractor->getTypes($currentClass, $attribute)) {
if (null === $types = $this->getTypes($currentClass, $attribute)) {
return $data;
}
$expectedTypes = [];
$isUnionType = \count($types) > 1;
$extraAttributesException = null;
$missingConstructorArgumentException = null;
foreach ($types as $type) {
if (null === $data && $type->isNullable()) {
return null;
@@ -252,6 +423,10 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
$data = [$data];
}
if (XmlEncoder::FORMAT === $format && '' === $data && Type::BUILTIN_TYPE_ARRAY === $type->getBuiltinType()) {
return [];
}
if (null !== $collectionValueType && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) {
$builtinType = Type::BUILTIN_TYPE_OBJECT;
$class = $collectionValueType->getClassName().'[]';
@@ -259,6 +434,26 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
if (null !== $collectionKeyType = $type->getCollectionKeyType()) {
$context['key_type'] = $collectionKeyType;
}
} elseif ($type->isCollection() && null !== ($collectionValueType = $type->getCollectionValueType()) && Type::BUILTIN_TYPE_ARRAY === $collectionValueType->getBuiltinType()) {
// get inner type for any nested array
$innerType = $collectionValueType;
// note that it will break for any other builtinType
$dimensions = '[]';
while (null !== $innerType->getCollectionValueType() && Type::BUILTIN_TYPE_ARRAY === $innerType->getBuiltinType()) {
$dimensions .= '[]';
$innerType = $innerType->getCollectionValueType();
}
if (null !== $innerType->getClassName()) {
// the builtinType is the inner one and the class is the class followed by []...[]
$builtinType = $innerType->getBuiltinType();
$class = $innerType->getClassName().$dimensions;
} else {
// default fallback (keep it as array)
$builtinType = $type->getBuiltinType();
$class = $type->getClassName();
}
} else {
$builtinType = $type->getBuiltinType();
$class = $type->getClassName();
@@ -266,33 +461,72 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
$expectedTypes[Type::BUILTIN_TYPE_OBJECT === $builtinType && $class ? $class : $builtinType] = true;
if (Type::BUILTIN_TYPE_OBJECT === $builtinType) {
if (!$this->serializer instanceof DenormalizerInterface) {
throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer.', $attribute, $class));
// This try-catch should cover all NotNormalizableValueException (and all return branches after the first
// exception) so we could try denormalizing all types of an union type. If the target type is not an union
// type, we will just re-throw the catched exception.
// In the case of no denormalization succeeds with an union type, it will fall back to the default exception
// with the acceptable types list.
try {
if (Type::BUILTIN_TYPE_OBJECT === $builtinType) {
if (!$this->serializer instanceof DenormalizerInterface) {
throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer.', $attribute, $class));
}
$childContext = $this->createChildContext($context, $attribute, $format);
if ($this->serializer->supportsDenormalization($data, $class, $format, $childContext)) {
return $this->serializer->denormalize($data, $class, $format, $childContext);
}
}
$childContext = $this->createChildContext($context, $attribute, $format);
if ($this->serializer->supportsDenormalization($data, $class, $format, $childContext)) {
return $this->serializer->denormalize($data, $class, $format, $childContext);
// JSON only has a Number type corresponding to both int and float PHP types.
// PHP's json_encode, JavaScript's JSON.stringify, Go's json.Marshal as well as most other JSON encoders convert
// floating-point numbers like 12.0 to 12 (the decimal part is dropped when possible).
// PHP's json_decode automatically converts Numbers without a decimal part to integers.
// To circumvent this behavior, integers are converted to floats when denormalizing JSON based formats and when
// a float is expected.
if (Type::BUILTIN_TYPE_FLOAT === $builtinType && \is_int($data) && null !== $format && str_contains($format, JsonEncoder::FORMAT)) {
return (float) $data;
}
}
// JSON only has a Number type corresponding to both int and float PHP types.
// PHP's json_encode, JavaScript's JSON.stringify, Go's json.Marshal as well as most other JSON encoders convert
// floating-point numbers like 12.0 to 12 (the decimal part is dropped when possible).
// PHP's json_decode automatically converts Numbers without a decimal part to integers.
// To circumvent this behavior, integers are converted to floats when denormalizing JSON based formats and when
// a float is expected.
if (Type::BUILTIN_TYPE_FLOAT === $builtinType && \is_int($data) && false !== strpos($format, JsonEncoder::FORMAT)) {
return (float) $data;
}
if ('false' === $builtinType && false === $data) {
return $data;
}
if (\call_user_func('is_'.$builtinType, $data)) {
return $data;
if (('is_'.$builtinType)($data)) {
return $data;
}
} catch (NotNormalizableValueException $e) {
if (!$isUnionType) {
throw $e;
}
} catch (ExtraAttributesException $e) {
if (!$isUnionType) {
throw $e;
}
if (!$extraAttributesException) {
$extraAttributesException = $e;
}
} catch (MissingConstructorArgumentsException $e) {
if (!$isUnionType) {
throw $e;
}
if (!$missingConstructorArgumentException) {
$missingConstructorArgumentException = $e;
}
}
}
if (!empty($context[self::DISABLE_TYPE_ENFORCEMENT])) {
if ($extraAttributesException) {
throw $extraAttributesException;
}
if ($missingConstructorArgumentException) {
throw $missingConstructorArgumentException;
}
if ($context[self::DISABLE_TYPE_ENFORCEMENT] ?? $this->defaultContext[self::DISABLE_TYPE_ENFORCEMENT] ?? false) {
return $data;
}
@@ -304,25 +538,65 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
*/
protected function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameter, $parameterName, $parameterData, array $context, $format = null)
{
if ((method_exists($parameter, 'isVariadic') && $parameter->isVariadic()) || null === $this->propertyTypeExtractor || null === $this->propertyTypeExtractor->getTypes($class->getName(), $parameterName)) {
if ($parameter->isVariadic() || null === $this->propertyTypeExtractor || null === $this->propertyTypeExtractor->getTypes($class->getName(), $parameterName)) {
return parent::denormalizeParameter($class, $parameter, $parameterName, $parameterData, $context, $format);
}
return $this->validateAndDenormalize($class->getName(), $parameterName, $parameterData, $format, $context);
$parameterData = $this->validateAndDenormalize($class->getName(), $parameterName, $parameterData, $format, $context);
return $this->applyCallbacks($parameterData, $class->getName(), $parameterName, $format, $context);
}
/**
* @return Type[]|null
*/
private function getTypes(string $currentClass, string $attribute): ?array
{
if (null === $this->propertyTypeExtractor) {
return null;
}
$key = $currentClass.'::'.$attribute;
if (isset($this->typesCache[$key])) {
return false === $this->typesCache[$key] ? null : $this->typesCache[$key];
}
if (null !== $types = $this->propertyTypeExtractor->getTypes($currentClass, $attribute)) {
return $this->typesCache[$key] = $types;
}
if (null !== $this->classDiscriminatorResolver && null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForClass($currentClass)) {
if ($discriminatorMapping->getTypeProperty() === $attribute) {
return $this->typesCache[$key] = [
new Type(Type::BUILTIN_TYPE_STRING),
];
}
foreach ($discriminatorMapping->getTypesMapping() as $mappedClass) {
if (null !== $types = $this->propertyTypeExtractor->getTypes($mappedClass, $attribute)) {
return $this->typesCache[$key] = $types;
}
}
}
$this->typesCache[$key] = false;
return null;
}
/**
* Sets an attribute and apply the name converter if necessary.
*
* @param string $attribute
* @param mixed $attributeValue
*
* @return array
* @param mixed $attributeValue
*/
private function updateData(array $data, $attribute, $attributeValue)
private function updateData(array $data, string $attribute, $attributeValue, string $class, ?string $format, array $context): array
{
if (null === $attributeValue && ($context[self::SKIP_NULL_VALUES] ?? $this->defaultContext[self::SKIP_NULL_VALUES] ?? false)) {
return $data;
}
if ($this->nameConverter) {
$attribute = $this->nameConverter->normalize($attribute);
$attribute = $this->nameConverter->normalize($attribute, $class, $format, $context);
}
$data[$attribute] = $attributeValue;
@@ -334,23 +608,19 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
* Is the max depth reached for the given attribute?
*
* @param AttributeMetadataInterface[] $attributesMetadata
* @param string $class
* @param string $attribute
*
* @return bool
*/
private function isMaxDepthReached(array $attributesMetadata, $class, $attribute, array &$context)
private function isMaxDepthReached(array $attributesMetadata, string $class, string $attribute, array &$context): bool
{
$enableMaxDepth = $context[self::ENABLE_MAX_DEPTH] ?? $this->defaultContext[self::ENABLE_MAX_DEPTH] ?? false;
if (
!isset($context[static::ENABLE_MAX_DEPTH]) ||
!$context[static::ENABLE_MAX_DEPTH] ||
!$enableMaxDepth ||
!isset($attributesMetadata[$attribute]) ||
null === $maxDepth = $attributesMetadata[$attribute]->getMaxDepth()
) {
return false;
}
$key = sprintf(static::DEPTH_KEY_PATTERN, $class, $attribute);
$key = sprintf(self::DEPTH_KEY_PATTERN, $class, $attribute);
if (!isset($context[$key])) {
$context[$key] = 1;
@@ -372,18 +642,21 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
* We must not mix up the attribute cache between parent and children.
*
* {@inheritdoc}
*
* @param string|null $format
*
* @internal
*/
protected function createChildContext(array $parentContext, $attribute/*, string $format = null */)
protected function createChildContext(array $parentContext, $attribute/* , ?string $format */): array
{
if (\func_num_args() >= 3) {
$format = func_get_arg(2);
} else {
// will be deprecated in version 4
@trigger_error(sprintf('Method "%s::%s()" will have a third "?string $format" argument in version 5.0; not defining it is deprecated since Symfony 4.3.', static::class, __FUNCTION__), \E_USER_DEPRECATED);
$format = null;
}
$context = parent::createChildContext($parentContext, $attribute, $format);
// format is already included in the cache_key of the parent.
$context['cache_key'] = $this->getCacheKey($format, $context);
return $context;
@@ -394,23 +667,37 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
*
* The key must be different for every option in the context that could change which attributes should be handled.
*
* @param string|null $format
*
* @return bool|string
*/
private function getCacheKey($format, array $context)
private function getCacheKey(?string $format, array $context)
{
foreach ($context[self::EXCLUDE_FROM_CACHE_KEY] ?? $this->defaultContext[self::EXCLUDE_FROM_CACHE_KEY] as $key) {
unset($context[$key]);
}
unset($context[self::EXCLUDE_FROM_CACHE_KEY]);
unset($context[self::OBJECT_TO_POPULATE]);
unset($context['cache_key']); // avoid artificially different keys
try {
return md5($format.serialize([
'context' => $context,
'ignored' => $this->ignoredAttributes,
'camelized' => $this->camelizedAttributes,
]));
} catch (\Exception $exception) {
} catch (\Exception $e) {
// The context cannot be serialized, skip the cache
return false;
}
}
/**
* This error may occur when specific object normalizer implementation gets attribute value
* by accessing a public uninitialized property or by calling a method accessing such property.
*/
private function isUninitializedValueError(\Error $e): bool
{
return \PHP_VERSION_ID >= 70400
&& str_starts_with($e->getMessage(), 'Typed property')
&& str_ends_with($e->getMessage(), 'must not be accessed before initialization');
}
}
+16 -8
View File
@@ -22,9 +22,9 @@ use Symfony\Component\Serializer\SerializerInterface;
*
* @author Alexander M. Turek <me@derrabus.de>
*
* @final since version 3.3.
* @final
*/
class ArrayDenormalizer implements DenormalizerInterface, SerializerAwareInterface
class ArrayDenormalizer implements ContextAwareDenormalizerInterface, SerializerAwareInterface, CacheableSupportsMethodInterface
{
/**
* @var SerializerInterface|DenormalizerInterface
@@ -35,6 +35,8 @@ class ArrayDenormalizer implements DenormalizerInterface, SerializerAwareInterfa
* {@inheritdoc}
*
* @throws NotNormalizableValueException
*
* @return array
*/
public function denormalize($data, $type, $format = null, array $context = [])
{
@@ -44,7 +46,7 @@ class ArrayDenormalizer implements DenormalizerInterface, SerializerAwareInterfa
if (!\is_array($data)) {
throw new InvalidArgumentException('Data expected to be an array, '.\gettype($data).' given.');
}
if ('[]' !== substr($type, -2)) {
if (!str_ends_with($type, '[]')) {
throw new InvalidArgumentException('Unsupported class: '.$type);
}
@@ -53,7 +55,7 @@ class ArrayDenormalizer implements DenormalizerInterface, SerializerAwareInterfa
$builtinType = isset($context['key_type']) ? $context['key_type']->getBuiltinType() : null;
foreach ($data as $key => $value) {
if (null !== $builtinType && !\call_user_func('is_'.$builtinType, $key)) {
if (null !== $builtinType && !('is_'.$builtinType)($key)) {
throw new NotNormalizableValueException(sprintf('The type of the key "%s" must be "%s" ("%s" given).', $key, $builtinType, \gettype($key)));
}
@@ -66,15 +68,13 @@ class ArrayDenormalizer implements DenormalizerInterface, SerializerAwareInterfa
/**
* {@inheritdoc}
*/
public function supportsDenormalization($data, $type, $format = null/*, array $context = []*/)
public function supportsDenormalization($data, $type, $format = null, array $context = []): bool
{
if (null === $this->serializer) {
throw new BadMethodCallException(sprintf('The serializer needs to be set to allow "%s()" to be used.', __METHOD__));
}
$context = \func_num_args() > 3 ? func_get_arg(3) : [];
return '[]' === substr($type, -2)
return str_ends_with($type, '[]')
&& $this->serializer->supportsDenormalization($data, substr($type, 0, -2), $format, $context);
}
@@ -89,4 +89,12 @@ class ArrayDenormalizer implements DenormalizerInterface, SerializerAwareInterfa
$this->serializer = $serializer;
}
/**
* {@inheritdoc}
*/
public function hasCacheableSupportsMethod(): bool
{
return $this->serializer instanceof CacheableSupportsMethodInterface && $this->serializer->hasCacheableSupportsMethod();
}
}
+10 -12
View File
@@ -17,13 +17,11 @@ use Symfony\Component\Serializer\SerializerAwareTrait;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class CustomNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
class CustomNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface, CacheableSupportsMethodInterface
{
use ObjectToPopulateTrait;
use SerializerAwareTrait;
private $cache = [];
/**
* {@inheritdoc}
*/
@@ -37,7 +35,7 @@ class CustomNormalizer implements NormalizerInterface, DenormalizerInterface, Se
*/
public function denormalize($data, $type, $format = null, array $context = [])
{
$object = $this->extractObjectToPopulate($type, $context) ?: new $type();
$object = $this->extractObjectToPopulate($type, $context) ?? new $type();
$object->denormalize($this->serializer, $data, $format, $context);
return $object;
@@ -67,14 +65,14 @@ class CustomNormalizer implements NormalizerInterface, DenormalizerInterface, Se
*/
public function supportsDenormalization($data, $type, $format = null)
{
if (isset($this->cache[$type])) {
return $this->cache[$type];
}
return is_subclass_of($type, DenormalizableInterface::class);
}
if (!class_exists($type)) {
return $this->cache[$type] = false;
}
return $this->cache[$type] = is_subclass_of($type, 'Symfony\Component\Serializer\Normalizer\DenormalizableInterface');
/**
* {@inheritdoc}
*/
public function hasCacheableSupportsMethod(): bool
{
return __CLASS__ === static::class;
}
}
+42 -16
View File
@@ -13,7 +13,9 @@ namespace Symfony\Component\Serializer\Normalizer;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesser;
use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface;
use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface as DeprecatedMimeTypeGuesserInterface;
use Symfony\Component\Mime\MimeTypeGuesserInterface;
use Symfony\Component\Mime\MimeTypes;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
@@ -23,9 +25,9 @@ use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class DataUriNormalizer implements NormalizerInterface, DenormalizerInterface
class DataUriNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface
{
private static $supportedTypes = [
private const SUPPORTED_TYPES = [
\SplFileInfo::class => true,
\SplFileObject::class => true,
File::class => true,
@@ -36,10 +38,22 @@ class DataUriNormalizer implements NormalizerInterface, DenormalizerInterface
*/
private $mimeTypeGuesser;
public function __construct(MimeTypeGuesserInterface $mimeTypeGuesser = null)
/**
* @param MimeTypeGuesserInterface|null $mimeTypeGuesser
*/
public function __construct($mimeTypeGuesser = null)
{
if (null === $mimeTypeGuesser && class_exists('Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesser')) {
$mimeTypeGuesser = MimeTypeGuesser::getInstance();
if ($mimeTypeGuesser instanceof DeprecatedMimeTypeGuesserInterface) {
@trigger_error(sprintf('Passing a %s to "%s()" is deprecated since Symfony 4.3, pass a "%s" instead.', DeprecatedMimeTypeGuesserInterface::class, __METHOD__, MimeTypeGuesserInterface::class), \E_USER_DEPRECATED);
} elseif (null === $mimeTypeGuesser) {
if (class_exists(MimeTypes::class)) {
$mimeTypeGuesser = MimeTypes::getDefault();
} elseif (class_exists(MimeTypeGuesser::class)) {
@trigger_error(sprintf('Passing null to "%s()" to use a default MIME type guesser without Symfony Mime installed is deprecated since Symfony 4.3. Try running "composer require symfony/mime".', __METHOD__), \E_USER_DEPRECATED);
$mimeTypeGuesser = MimeTypeGuesser::getInstance();
}
} elseif (!$mimeTypeGuesser instanceof MimeTypes) {
throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be an instance of "%s" or null, "%s" given.', __METHOD__, MimeTypes::class, \is_object($mimeTypeGuesser) ? \get_class($mimeTypeGuesser) : \gettype($mimeTypeGuesser)));
}
$this->mimeTypeGuesser = $mimeTypeGuesser;
@@ -47,6 +61,8 @@ class DataUriNormalizer implements NormalizerInterface, DenormalizerInterface
/**
* {@inheritdoc}
*
* @return string
*/
public function normalize($object, $format = null, array $context = [])
{
@@ -88,16 +104,18 @@ class DataUriNormalizer implements NormalizerInterface, DenormalizerInterface
*
* @throws InvalidArgumentException
* @throws NotNormalizableValueException
*
* @return \SplFileInfo
*/
public function denormalize($data, $type, $format = null, array $context = [])
{
if (!preg_match('/^data:([a-z0-9][a-z0-9\!\#\$\&\-\^\_\+\.]{0,126}\/[a-z0-9][a-z0-9\!\#\$\&\-\^\_\+\.]{0,126}(;[a-z0-9\-]+\=[a-z0-9\-]+)?)?(;base64)?,[a-z0-9\!\$\&\\\'\,\(\)\*\+\,\;\=\-\.\_\~\:\@\/\?\%\s]*\s*$/i', $data)) {
if (null === $data || !preg_match('/^data:([a-z0-9][a-z0-9\!\#\$\&\-\^\_\+\.]{0,126}\/[a-z0-9][a-z0-9\!\#\$\&\-\^\_\+\.]{0,126}(;[a-z0-9\-]+\=[a-z0-9\-]+)?)?(;base64)?,[a-z0-9\!\$\&\\\'\,\(\)\*\+\,\;\=\-\.\_\~\:\@\/\?\%\s]*\s*$/i', $data)) {
throw new NotNormalizableValueException('The provided "data:" URI is not valid.');
}
try {
switch ($type) {
case 'Symfony\Component\HttpFoundation\File\File':
case File::class:
return new File($data, false);
case 'SplFileObject':
@@ -116,21 +134,31 @@ class DataUriNormalizer implements NormalizerInterface, DenormalizerInterface
*/
public function supportsDenormalization($data, $type, $format = null)
{
return isset(self::$supportedTypes[$type]);
return isset(self::SUPPORTED_TYPES[$type]);
}
/**
* {@inheritdoc}
*/
public function hasCacheableSupportsMethod(): bool
{
return __CLASS__ === static::class;
}
/**
* Gets the mime type of the object. Defaults to application/octet-stream.
*
* @return string
*/
private function getMimeType(\SplFileInfo $object)
private function getMimeType(\SplFileInfo $object): string
{
if ($object instanceof File) {
return $object->getMimeType();
}
if ($this->mimeTypeGuesser && $mimeType = $this->mimeTypeGuesser->guess($object->getPathname())) {
if ($this->mimeTypeGuesser instanceof DeprecatedMimeTypeGuesserInterface && $mimeType = $this->mimeTypeGuesser->guess($object->getPathname())) {
return $mimeType;
}
if ($this->mimeTypeGuesser && $mimeType = $this->mimeTypeGuesser->guessMimeType($object->getPathname())) {
return $mimeType;
}
@@ -139,10 +167,8 @@ class DataUriNormalizer implements NormalizerInterface, DenormalizerInterface
/**
* Returns the \SplFileObject instance associated with the given \SplFileInfo instance.
*
* @return \SplFileObject
*/
private function extractSplFileObject(\SplFileInfo $object)
private function extractSplFileObject(\SplFileInfo $object): \SplFileObject
{
if ($object instanceof \SplFileObject) {
return $object;
@@ -20,24 +20,34 @@ use Symfony\Component\Serializer\Exception\UnexpectedValueException;
*
* @author Jérôme Parmentier <jerome@prmntr.me>
*/
class DateIntervalNormalizer implements NormalizerInterface, DenormalizerInterface
class DateIntervalNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface
{
const FORMAT_KEY = 'dateinterval_format';
public const FORMAT_KEY = 'dateinterval_format';
private $format;
private $defaultContext = [
self::FORMAT_KEY => '%rP%yY%mM%dDT%hH%iM%sS',
];
/**
* @param string $format
* @param array $defaultContext
*/
public function __construct($format = '%rP%yY%mM%dDT%hH%iM%sS')
public function __construct($defaultContext = [])
{
$this->format = $format;
if (!\is_array($defaultContext)) {
@trigger_error(sprintf('The "format" parameter is deprecated since Symfony 4.2, use the "%s" key of the context instead.', self::FORMAT_KEY), \E_USER_DEPRECATED);
$defaultContext = [self::FORMAT_KEY => (string) $defaultContext];
}
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
}
/**
* {@inheritdoc}
*
* @throws InvalidArgumentException
*
* @return string
*/
public function normalize($object, $format = null, array $context = [])
{
@@ -45,9 +55,7 @@ class DateIntervalNormalizer implements NormalizerInterface, DenormalizerInterfa
throw new InvalidArgumentException('The object must be an instance of "\DateInterval".');
}
$dateIntervalFormat = isset($context[self::FORMAT_KEY]) ? $context[self::FORMAT_KEY] : $this->format;
return $object->format($dateIntervalFormat);
return $object->format($context[self::FORMAT_KEY] ?? $this->defaultContext[self::FORMAT_KEY]);
}
/**
@@ -58,11 +66,21 @@ class DateIntervalNormalizer implements NormalizerInterface, DenormalizerInterfa
return $data instanceof \DateInterval;
}
/**
* {@inheritdoc}
*/
public function hasCacheableSupportsMethod(): bool
{
return __CLASS__ === static::class;
}
/**
* {@inheritdoc}
*
* @throws InvalidArgumentException
* @throws UnexpectedValueException
*
* @return \DateInterval
*/
public function denormalize($data, $type, $format = null, array $context = [])
{
@@ -74,7 +92,7 @@ class DateIntervalNormalizer implements NormalizerInterface, DenormalizerInterfa
throw new UnexpectedValueException('Expected a valid ISO 8601 interval string.');
}
$dateIntervalFormat = isset($context[self::FORMAT_KEY]) ? $context[self::FORMAT_KEY] : $this->format;
$dateIntervalFormat = $context[self::FORMAT_KEY] ?? $this->defaultContext[self::FORMAT_KEY];
$signPattern = '';
switch (substr($dateIntervalFormat, 0, 2)) {
@@ -87,7 +105,7 @@ class DateIntervalNormalizer implements NormalizerInterface, DenormalizerInterfa
$dateIntervalFormat = substr($dateIntervalFormat, 2);
break;
}
$valuePattern = '/^'.$signPattern.preg_replace('/%([yYmMdDhHiIsSwW])(\w)/', '(?P<$1>\d+)$2', $dateIntervalFormat).'$/';
$valuePattern = '/^'.$signPattern.preg_replace('/%([yYmMdDhHiIsSwW])(\w)/', '(?:(?P<$1>\d+)$2)?', preg_replace('/(T.*)$/', '($1)?', $dateIntervalFormat)).'$/';
if (!preg_match($valuePattern, $data)) {
throw new UnexpectedValueException(sprintf('Value "%s" contains intervals not accepted by format "%s".', $data, $dateIntervalFormat));
}
@@ -118,7 +136,7 @@ class DateIntervalNormalizer implements NormalizerInterface, DenormalizerInterfa
return \DateInterval::class === $type;
}
private function isISO8601($string)
private function isISO8601(string $string): bool
{
return preg_match('/^[\-+]?P(?=\w*(?:\d|%\w))(?:\d+Y|%[yY]Y)?(?:\d+M|%[mM]M)?(?:(?:\d+D|%[dD]D)|(?:\d+W|%[wW]W))?(?:T(?:\d+H|[hH]H)?(?:\d+M|[iI]M)?(?:\d+S|[sS]S)?)?$/', $string);
}
+51 -24
View File
@@ -20,33 +20,45 @@ use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface
class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface
{
const FORMAT_KEY = 'datetime_format';
const TIMEZONE_KEY = 'datetime_timezone';
public const FORMAT_KEY = 'datetime_format';
public const TIMEZONE_KEY = 'datetime_timezone';
private $format;
private $timezone;
private $defaultContext;
private static $supportedTypes = [
private const SUPPORTED_TYPES = [
\DateTimeInterface::class => true,
\DateTimeImmutable::class => true,
\DateTime::class => true,
];
/**
* @param string $format
* @param array $defaultContext
*/
public function __construct($format = \DateTime::RFC3339, \DateTimeZone $timezone = null)
public function __construct($defaultContext = [], \DateTimeZone $timezone = null)
{
$this->format = $format;
$this->timezone = $timezone;
$this->defaultContext = [
self::FORMAT_KEY => \DateTime::RFC3339,
self::TIMEZONE_KEY => null,
];
if (!\is_array($defaultContext)) {
@trigger_error('Passing configuration options directly to the constructor is deprecated since Symfony 4.2, use the default context instead.', \E_USER_DEPRECATED);
$defaultContext = [self::FORMAT_KEY => (string) $defaultContext];
$defaultContext[self::TIMEZONE_KEY] = $timezone;
}
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
}
/**
* {@inheritdoc}
*
* @throws InvalidArgumentException
*
* @return string
*/
public function normalize($object, $format = null, array $context = [])
{
@@ -54,7 +66,7 @@ class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface
throw new InvalidArgumentException('The object must implement the "\DateTimeInterface".');
}
$format = isset($context[self::FORMAT_KEY]) ? $context[self::FORMAT_KEY] : $this->format;
$dateTimeFormat = $context[self::FORMAT_KEY] ?? $this->defaultContext[self::FORMAT_KEY];
$timezone = $this->getTimezone($context);
if (null !== $timezone) {
@@ -62,7 +74,7 @@ class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface
$object = $object->setTimezone($timezone);
}
return $object->format($format);
return $object->format($dateTimeFormat);
}
/**
@@ -77,23 +89,20 @@ class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface
* {@inheritdoc}
*
* @throws NotNormalizableValueException
*
* @return \DateTimeInterface
*/
public function denormalize($data, $type, $format = null, array $context = [])
{
$dateTimeFormat = isset($context[self::FORMAT_KEY]) ? $context[self::FORMAT_KEY] : null;
$dateTimeFormat = $context[self::FORMAT_KEY] ?? null;
$timezone = $this->getTimezone($context);
if ('' === $data || null === $data) {
if (null === $data || (\is_string($data) && '' === trim($data))) {
throw new NotNormalizableValueException('The data is either an empty string or null, you should pass a string that can be parsed with the passed format or a valid DateTime string.');
}
if (null !== $dateTimeFormat) {
if (null === $timezone && \PHP_VERSION_ID < 70000) {
// https://bugs.php.net/68669
$object = \DateTime::class === $type ? \DateTime::createFromFormat($dateTimeFormat, $data) : \DateTimeImmutable::createFromFormat($dateTimeFormat, $data);
} else {
$object = \DateTime::class === $type ? \DateTime::createFromFormat($dateTimeFormat, $data, $timezone) : \DateTimeImmutable::createFromFormat($dateTimeFormat, $data, $timezone);
}
$object = \DateTime::class === $type ? \DateTime::createFromFormat($dateTimeFormat, $data, $timezone) : \DateTimeImmutable::createFromFormat($dateTimeFormat, $data, $timezone);
if (false !== $object) {
return $object;
@@ -104,6 +113,16 @@ class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface
throw new NotNormalizableValueException(sprintf('Parsing datetime string "%s" using format "%s" resulted in %d errors: ', $data, $dateTimeFormat, $dateTimeErrors['error_count'])."\n".implode("\n", $this->formatDateTimeErrors($dateTimeErrors['errors'])));
}
$defaultDateTimeFormat = $this->defaultContext[self::FORMAT_KEY] ?? null;
if (null !== $defaultDateTimeFormat) {
$object = \DateTime::class === $type ? \DateTime::createFromFormat($defaultDateTimeFormat, $data, $timezone) : \DateTimeImmutable::createFromFormat($defaultDateTimeFormat, $data, $timezone);
if (false !== $object) {
return $object;
}
}
try {
return \DateTime::class === $type ? new \DateTime($data, $timezone) : new \DateTimeImmutable($data, $timezone);
} catch (\Exception $e) {
@@ -116,7 +135,15 @@ class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface
*/
public function supportsDenormalization($data, $type, $format = null)
{
return isset(self::$supportedTypes[$type]);
return isset(self::SUPPORTED_TYPES[$type]);
}
/**
* {@inheritdoc}
*/
public function hasCacheableSupportsMethod(): bool
{
return __CLASS__ === static::class;
}
/**
@@ -124,7 +151,7 @@ class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface
*
* @return string[]
*/
private function formatDateTimeErrors(array $errors)
private function formatDateTimeErrors(array $errors): array
{
$formattedErrors = [];
@@ -135,9 +162,9 @@ class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface
return $formattedErrors;
}
private function getTimezone(array $context)
private function getTimezone(array $context): ?\DateTimeZone
{
$dateTimeZone = \array_key_exists(self::TIMEZONE_KEY, $context) ? $context[self::TIMEZONE_KEY] : $this->timezone;
$dateTimeZone = $context[self::TIMEZONE_KEY] ?? $this->defaultContext[self::TIMEZONE_KEY];
if (null === $dateTimeZone) {
return null;
@@ -33,8 +33,6 @@ interface DenormalizableInterface
* @param string|null $format The format is optionally given to be able to denormalize
* differently based on different input formats
* @param array $context Options for denormalizing
*
* @return object|object[]
*/
public function denormalize(DenormalizerInterface $denormalizer, $data, $format = null, array $context = []);
}
@@ -21,11 +21,6 @@ trait DenormalizerAwareTrait
*/
protected $denormalizer;
/**
* Sets the Denormalizer.
*
* @param DenormalizerInterface $denormalizer A DenormalizerInterface instance
*/
public function setDenormalizer(DenormalizerInterface $denormalizer)
{
$this->denormalizer = $denormalizer;
@@ -35,14 +35,13 @@ namespace Symfony\Component\Serializer\Normalizer;
class GetSetMethodNormalizer extends AbstractObjectNormalizer
{
private static $setterAccessibleCache = [];
private $cache = [];
/**
* {@inheritdoc}
*/
public function supportsNormalization($data, $format = null)
{
return parent::supportsNormalization($data, $format) && (isset($this->cache[$type = \get_class($data)]) ? $this->cache[$type] : $this->cache[$type] = $this->supports($type));
return parent::supportsNormalization($data, $format) && $this->supports(\get_class($data));
}
/**
@@ -50,17 +49,21 @@ class GetSetMethodNormalizer extends AbstractObjectNormalizer
*/
public function supportsDenormalization($data, $type, $format = null)
{
return parent::supportsDenormalization($data, $type, $format) && (isset($this->cache[$type]) ? $this->cache[$type] : $this->cache[$type] = $this->supports($type));
return parent::supportsDenormalization($data, $type, $format) && $this->supports($type);
}
/**
* Checks if the given class has any get{Property} method.
*
* @param string $class
*
* @return bool
* {@inheritdoc}
*/
private function supports($class)
public function hasCacheableSupportsMethod(): bool
{
return __CLASS__ === static::class;
}
/**
* Checks if the given class has any getter method.
*/
private function supports(string $class): bool
{
$class = new \ReflectionClass($class);
$methods = $class->getMethods(\ReflectionMethod::IS_PUBLIC);
@@ -74,20 +77,18 @@ class GetSetMethodNormalizer extends AbstractObjectNormalizer
}
/**
* Checks if a method's name is get.* or is.*, and can be called without parameters.
*
* @return bool whether the method is a getter or boolean getter
* Checks if a method's name matches /^(get|is|has).+$/ and can be called non-statically without parameters.
*/
private function isGetMethod(\ReflectionMethod $method)
private function isGetMethod(\ReflectionMethod $method): bool
{
$methodLength = \strlen($method->name);
return
!$method->isStatic() &&
(
((0 === strpos($method->name, 'get') && 3 < $methodLength) ||
(0 === strpos($method->name, 'is') && 2 < $methodLength) ||
(0 === strpos($method->name, 'has') && 3 < $methodLength)) &&
((str_starts_with($method->name, 'get') && 3 < $methodLength) ||
(str_starts_with($method->name, 'is') && 2 < $methodLength) ||
(str_starts_with($method->name, 'has') && 3 < $methodLength)) &&
0 === $method->getNumberOfRequiredParameters()
)
;
@@ -107,7 +108,7 @@ class GetSetMethodNormalizer extends AbstractObjectNormalizer
continue;
}
$attributeName = lcfirst(substr($method->name, 0 === strpos($method->name, 'is') ? 2 : 3));
$attributeName = lcfirst(substr($method->name, str_starts_with($method->name, 'is') ? 2 : 3));
if ($this->isAllowedAttribute($object, $attributeName, $format, $context)) {
$attributes[] = $attributeName;
@@ -27,7 +27,7 @@ class JsonSerializableNormalizer extends AbstractNormalizer
public function normalize($object, $format = null, array $context = [])
{
if ($this->isCircularReference($object, $context)) {
return $this->handleCircularReference($object);
return $this->handleCircularReference($object, $format, $context);
}
if (!$object instanceof \JsonSerializable) {
@@ -64,4 +64,12 @@ class JsonSerializableNormalizer extends AbstractNormalizer
{
throw new LogicException(sprintf('Cannot denormalize with "%s".', \JsonSerializable::class));
}
/**
* {@inheritdoc}
*/
public function hasCacheableSupportsMethod(): bool
{
return __CLASS__ === static::class;
}
}
@@ -21,11 +21,6 @@ trait NormalizerAwareTrait
*/
protected $normalizer;
/**
* Sets the normalizer.
*
* @param NormalizerInterface $normalizer A NormalizerInterface instance
*/
public function setNormalizer(NormalizerInterface $normalizer)
{
$this->normalizer = $normalizer;
@@ -28,7 +28,7 @@ interface NormalizerInterface
* @param string $format Format the normalization result will be encoded as
* @param array $context Context options for the normalizer
*
* @return array|string|int|float|bool|null
* @return array|string|int|float|bool|\ArrayObject|null \ArrayObject is used to make sure an empty object is encoded as an object not an array
*
* @throws InvalidArgumentException Occurs when the object given is not a supported type for the normalizer
* @throws CircularReferenceException Occurs when the normalizer detects a circular reference when no circular
+64 -12
View File
@@ -15,7 +15,9 @@ use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\Serializer\Exception\RuntimeException;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
@@ -28,15 +30,31 @@ class ObjectNormalizer extends AbstractObjectNormalizer
{
protected $propertyAccessor;
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null)
private $discriminatorCache = [];
private $objectClassResolver;
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = [])
{
if (!class_exists(PropertyAccess::class)) {
throw new RuntimeException('The ObjectNormalizer class requires the "PropertyAccess" component. Install "symfony/property-access" to use it.');
throw new LogicException('The ObjectNormalizer class requires the "PropertyAccess" component. Install "symfony/property-access" to use it.');
}
parent::__construct($classMetadataFactory, $nameConverter, $propertyTypeExtractor);
parent::__construct($classMetadataFactory, $nameConverter, $propertyTypeExtractor, $classDiscriminatorResolver, $objectClassResolver, $defaultContext);
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
$this->objectClassResolver = $objectClassResolver ?? function ($class) {
return \is_object($class) ? \get_class($class) : $class;
};
}
/**
* {@inheritdoc}
*/
public function hasCacheableSupportsMethod(): bool
{
return __CLASS__ === static::class;
}
/**
@@ -48,7 +66,9 @@ class ObjectNormalizer extends AbstractObjectNormalizer
$attributes = [];
// methods
$reflClass = new \ReflectionClass($object);
$class = ($this->objectClassResolver)($object);
$reflClass = new \ReflectionClass($class);
foreach ($reflClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflMethod) {
if (
0 !== $reflMethod->getNumberOfRequiredParameters() ||
@@ -62,14 +82,14 @@ class ObjectNormalizer extends AbstractObjectNormalizer
$name = $reflMethod->name;
$attributeName = null;
if (0 === strpos($name, 'get') || 0 === strpos($name, 'has')) {
if (str_starts_with($name, 'get') || str_starts_with($name, 'has')) {
// getters and hassers
$attributeName = substr($name, 3);
if (!$reflClass->hasProperty($attributeName)) {
$attributeName = lcfirst($attributeName);
}
} elseif (0 === strpos($name, 'is')) {
} elseif (str_starts_with($name, 'is')) {
// issers
$attributeName = substr($name, 2);
@@ -83,11 +103,9 @@ class ObjectNormalizer extends AbstractObjectNormalizer
}
}
$checkPropertyInitialization = \PHP_VERSION_ID >= 70400;
// properties
foreach ($reflClass->getProperties(\ReflectionProperty::IS_PUBLIC) as $reflProperty) {
if ($checkPropertyInitialization && !$reflProperty->isInitialized($object)) {
foreach ($reflClass->getProperties() as $reflProperty) {
if (!$reflProperty->isPublic()) {
continue;
}
@@ -106,7 +124,16 @@ class ObjectNormalizer extends AbstractObjectNormalizer
*/
protected function getAttributeValue($object, $attribute, $format = null, array $context = [])
{
return $this->propertyAccessor->getValue($object, $attribute);
$cacheKey = \get_class($object);
if (!\array_key_exists($cacheKey, $this->discriminatorCache)) {
$this->discriminatorCache[$cacheKey] = null;
if (null !== $this->classDiscriminatorResolver) {
$mapping = $this->classDiscriminatorResolver->getMappingForMappedObject($object);
$this->discriminatorCache[$cacheKey] = null === $mapping ? null : $mapping->getTypeProperty();
}
}
return $attribute === $this->discriminatorCache[$cacheKey] ? $this->classDiscriminatorResolver->getTypeForMappedObject($object) : $this->propertyAccessor->getValue($object, $attribute);
}
/**
@@ -120,4 +147,29 @@ class ObjectNormalizer extends AbstractObjectNormalizer
// Properties not found are ignored
}
}
/**
* {@inheritdoc}
*/
protected function getAllowedAttributes($classOrObject, array $context, $attributesAsString = false)
{
if (false === $allowedAttributes = parent::getAllowedAttributes($classOrObject, $context, $attributesAsString)) {
return false;
}
if (null !== $this->classDiscriminatorResolver) {
$class = \is_object($classOrObject) ? \get_class($classOrObject) : $classOrObject;
if (null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForMappedObject($classOrObject)) {
$allowedAttributes[] = $attributesAsString ? $discriminatorMapping->getTypeProperty() : new AttributeMetadata($discriminatorMapping->getTypeProperty());
}
if (null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForClass($class)) {
foreach ($discriminatorMapping->getTypesMapping() as $mappedClass) {
$allowedAttributes = array_merge($allowedAttributes, parent::getAllowedAttributes($mappedClass, $context, $attributesAsString));
}
}
}
return $allowedAttributes;
}
}
@@ -25,7 +25,7 @@ trait ObjectToPopulateTrait
*/
protected function extractObjectToPopulate($class, array $context, $key = null)
{
$key = $key ?: 'object_to_populate';
$key = $key ?? AbstractNormalizer::OBJECT_TO_POPULATE;
if (isset($context[$key]) && \is_object($context[$key]) && $context[$key] instanceof $class) {
return $context[$key];
+30 -25
View File
@@ -11,6 +11,8 @@
namespace Symfony\Component\Serializer\Normalizer;
use Symfony\Component\PropertyAccess\Exception\AccessException;
/**
* Converts between objects and arrays by mapping properties.
*
@@ -30,14 +32,12 @@ namespace Symfony\Component\Serializer\Normalizer;
*/
class PropertyNormalizer extends AbstractObjectNormalizer
{
private $cache = [];
/**
* {@inheritdoc}
*/
public function supportsNormalization($data, $format = null)
{
return parent::supportsNormalization($data, $format) && (isset($this->cache[$type = \get_class($data)]) ? $this->cache[$type] : $this->cache[$type] = $this->supports($type));
return parent::supportsNormalization($data, $format) && $this->supports(\get_class($data));
}
/**
@@ -45,17 +45,21 @@ class PropertyNormalizer extends AbstractObjectNormalizer
*/
public function supportsDenormalization($data, $type, $format = null)
{
return parent::supportsDenormalization($data, $type, $format) && (isset($this->cache[$type]) ? $this->cache[$type] : $this->cache[$type] = $this->supports($type));
return parent::supportsDenormalization($data, $type, $format) && $this->supports($type);
}
/**
* {@inheritdoc}
*/
public function hasCacheableSupportsMethod(): bool
{
return __CLASS__ === static::class;
}
/**
* Checks if the given class has any non-static property.
*
* @param string $class
*
* @return bool
*/
private function supports($class)
private function supports(string $class): bool
{
$class = new \ReflectionClass($class);
@@ -99,20 +103,9 @@ class PropertyNormalizer extends AbstractObjectNormalizer
{
$reflectionObject = new \ReflectionObject($object);
$attributes = [];
$checkPropertyInitialization = \PHP_VERSION_ID >= 70400;
do {
foreach ($reflectionObject->getProperties() as $property) {
if ($checkPropertyInitialization) {
if (!$property->isPublic()) {
$property->setAccessible(true);
}
if (!$property->isInitialized($object)) {
continue;
}
}
if (!$this->isAllowedAttribute($reflectionObject->getName(), $property->name, $format, $context)) {
continue;
}
@@ -121,7 +114,7 @@ class PropertyNormalizer extends AbstractObjectNormalizer
}
} while ($reflectionObject = $reflectionObject->getParentClass());
return $attributes;
return array_unique($attributes);
}
/**
@@ -140,6 +133,21 @@ class PropertyNormalizer extends AbstractObjectNormalizer
$reflectionProperty->setAccessible(true);
}
if (\PHP_VERSION_ID >= 70400 && $reflectionProperty->hasType()) {
return $reflectionProperty->getValue($object);
}
if (!method_exists($object, '__get') && !isset($object->$attribute)) {
$propertyValues = (array) $object;
if (($reflectionProperty->isPublic() && !\array_key_exists($reflectionProperty->name, $propertyValues))
|| ($reflectionProperty->isProtected() && !\array_key_exists("\0*\0{$reflectionProperty->name}", $propertyValues))
|| ($reflectionProperty->isPrivate() && !\array_key_exists("\0{$reflectionProperty->class}\0{$reflectionProperty->name}", $propertyValues))
) {
throw new AccessException(sprintf('The property "%s::$%s" is not initialized.', \get_class($object), $reflectionProperty->name));
}
}
return $reflectionProperty->getValue($object);
}
@@ -168,13 +176,10 @@ class PropertyNormalizer extends AbstractObjectNormalizer
/**
* @param string|object $classOrObject
* @param string $attribute
*
* @return \ReflectionProperty
*
* @throws \ReflectionException
*/
private function getReflectionProperty($classOrObject, $attribute)
private function getReflectionProperty($classOrObject, string $attribute): \ReflectionProperty
{
$reflectionClass = new \ReflectionClass($classOrObject);
while (true) {
@@ -1,27 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Normalizer;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerAwareTrait;
/**
* SerializerAware Normalizer implementation.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @deprecated since version 3.1, to be removed in 4.0. Use the SerializerAwareTrait instead.
*/
abstract class SerializerAwareNormalizer implements SerializerAwareInterface
{
use SerializerAwareTrait;
}