first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,582 @@
<?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\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\NotNormalizableValueException;
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 implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface, CacheableSupportsMethodInterface
{
use ObjectToPopulateTrait;
use SerializerAwareTrait;
/* constants to configure the context */
/**
* 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';
/**
* Require all properties to be listed in the input instead of falling
* back to null for nullable ones.
*/
public const REQUIRE_ALL_PROPERTIES = 'require_all_properties';
/**
* @internal
*/
protected const CIRCULAR_REFERENCE_LIMIT_COUNTERS = 'circular_reference_limit_counters';
protected $defaultContext = [
self::ALLOW_EXTRA_ATTRIBUTES => true,
self::CIRCULAR_REFERENCE_HANDLER => null,
self::CIRCULAR_REFERENCE_LIMIT => 1,
self::IGNORED_ATTRIBUTES => [],
];
/**
* @var ClassMetadataFactoryInterface|null
*/
protected $classMetadataFactory;
/**
* @var NameConverterInterface|null
*/
protected $nameConverter;
/**
* Sets the {@link ClassMetadataFactoryInterface} to use.
*/
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));
}
}
/**
* @deprecated since Symfony 6.3, use "getSupportedTypes()" instead
*/
public function hasCacheableSupportsMethod(): bool
{
trigger_deprecation('symfony/serializer', '6.3', 'The "%s()" method is deprecated, implement "%s::getSupportedTypes()" instead.', __METHOD__, get_debug_type($this));
return false;
}
/**
* Detects if the configured circular reference limit is reached.
*
* @throws CircularReferenceException
*/
protected function isCircularReference(object $object, array &$context): bool
{
$objectHash = spl_object_hash($object);
$circularReferenceLimit = $context[self::CIRCULAR_REFERENCE_LIMIT] ?? $this->defaultContext[self::CIRCULAR_REFERENCE_LIMIT];
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[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash];
} else {
$context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash] = 1;
}
return false;
}
/**
* Handles a circular reference.
*
* If a circular reference handler is set, it will be called. Otherwise, a
* {@class CircularReferenceException} will be thrown.
*
* @final
*
* @throws CircularReferenceException
*/
protected function handleCircularReference(object $object, ?string $format = null, array $context = []): mixed
{
$circularReferenceHandler = $context[self::CIRCULAR_REFERENCE_HANDLER] ?? $this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER];
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_debug_type($object), $context[self::CIRCULAR_REFERENCE_LIMIT] ?? $this->defaultContext[self::CIRCULAR_REFERENCE_LIMIT]));
}
/**
* Gets attributes to normalize using groups.
*
* @param bool $attributesAsString If false, return an array of {@link AttributeMetadataInterface}
*
* @return string[]|AttributeMetadataInterface[]|bool
*
* @throws LogicException if the 'allow_extra_attributes' context variable is false and no class metadata factory is provided
*/
protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false)
{
$allowExtraAttributes = $context[self::ALLOW_EXTRA_ATTRIBUTES] ?? $this->defaultContext[self::ALLOW_EXTRA_ATTRIBUTES];
if (!$this->classMetadataFactory) {
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 = $this->getGroups($context);
$allowedAttributes = [];
$ignoreUsed = false;
foreach ($this->classMetadataFactory->getMetadataFor($classOrObject)->getAttributesMetadata() as $attributeMetadata) {
if ($ignore = $attributeMetadata->isIgnored()) {
$ignoreUsed = true;
}
// If you update this check, update accordingly the one in Symfony\Component\PropertyInfo\Extractor\SerializerExtractor::getProperties()
if (
!$ignore
&& ([] === $groups || array_intersect(array_merge($attributeMetadata->getGroups(), ['*']), $groups))
&& $this->isAllowedAttribute($classOrObject, $name = $attributeMetadata->getName(), null, $context)
) {
$allowedAttributes[] = $attributesAsString ? $name : $attributeMetadata;
}
}
if (!$ignoreUsed && [] === $groups && $allowExtraAttributes) {
// Backward Compatibility with the code using this method written before the introduction of @Ignore
return false;
}
return $allowedAttributes;
}
protected function getGroups(array $context): array
{
$groups = $context[self::GROUPS] ?? $this->defaultContext[self::GROUPS] ?? [];
return \is_scalar($groups) ? (array) $groups : $groups;
}
/**
* Is this attribute allowed?
*/
protected function isAllowedAttribute(object|string $classOrObject, string $attribute, ?string $format = null, array $context = [])
{
$ignoredAttributes = $context[self::IGNORED_ATTRIBUTES] ?? $this->defaultContext[self::IGNORED_ATTRIBUTES];
if (\in_array($attribute, $ignoredAttributes)) {
return false;
}
$attributes = $context[self::ATTRIBUTES] ?? $this->defaultContext[self::ATTRIBUTES] ?? null;
if (isset($attributes[$attribute])) {
// Nested attributes
return true;
}
if (\is_array($attributes)) {
return \in_array($attribute, $attributes, true);
}
return true;
}
/**
* Normalizes the given data to an array. It's particularly useful during
* the denormalization process.
*/
protected function prepareForDenormalization(mixed $data): array
{
return (array) $data;
}
/**
* Returns the method to use to construct an object. This method must be either
* the object constructor or static.
*/
protected function getConstructor(array &$data, string $class, array &$context, \ReflectionClass $reflectionClass, array|bool $allowedAttributes): ?\ReflectionMethod
{
return $reflectionClass->getConstructor();
}
/**
* Instantiates an object using constructor parameters when needed.
*
* This method also allows to denormalize data into an existing object if
* it is present in the context with the object_to_populate. This object
* is removed from the context before being returned to avoid side effects
* when recursively normalizing an object graph.
*
* @return object
*
* @throws RuntimeException
* @throws MissingConstructorArgumentsException
*/
protected function instantiateObject(array &$data, string $class, array &$context, \ReflectionClass $reflectionClass, array|bool $allowedAttributes, ?string $format = null)
{
if (null !== $object = $this->extractObjectToPopulate($class, $context, self::OBJECT_TO_POPULATE)) {
unset($context[self::OBJECT_TO_POPULATE]);
return $object;
}
// clean up even if no match
unset($context[static::OBJECT_TO_POPULATE]);
$constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes);
if ($constructor) {
$context['has_constructor'] = true;
if (true !== $constructor->isPublic()) {
return $reflectionClass->newInstanceWithoutConstructor();
}
$constructorParameters = $constructor->getParameters();
$missingConstructorArguments = [];
$params = [];
$unsetKeys = [];
foreach ($constructorParameters as $constructorParameter) {
$paramName = $constructorParameter->name;
$attributeContext = $this->getAttributeDenormalizationContext($class, $paramName, $context);
$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 ($constructorParameter->isVariadic()) {
if ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
if (!\is_array($data[$key])) {
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));
}
$variadicParameters = [];
foreach ($data[$key] as $parameterKey => $parameterData) {
$variadicParameters[$parameterKey] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $attributeContext, $format);
}
$params = array_merge(array_values($params), $variadicParameters);
$unsetKeys[] = $key;
}
} elseif ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
$parameterData = $data[$key];
if (null === $parameterData && $constructorParameter->allowsNull()) {
$params[$paramName] = null;
$unsetKeys[] = $key;
continue;
}
try {
$params[$paramName] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $attributeContext, $format);
} catch (NotNormalizableValueException $exception) {
if (!isset($context['not_normalizable_value_exceptions'])) {
throw $exception;
}
$context['not_normalizable_value_exceptions'][] = $exception;
$params[$paramName] = $parameterData;
}
$unsetKeys[] = $key;
} elseif (\array_key_exists($key, $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class] ?? [])) {
$params[$paramName] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
} elseif (\array_key_exists($key, $this->defaultContext[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class] ?? [])) {
$params[$paramName] = $this->defaultContext[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
} elseif ($constructorParameter->isDefaultValueAvailable()) {
$params[$paramName] = $constructorParameter->getDefaultValue();
} elseif (!($context[self::REQUIRE_ALL_PROPERTIES] ?? $this->defaultContext[self::REQUIRE_ALL_PROPERTIES] ?? false) && $constructorParameter->hasType() && $constructorParameter->getType()->allowsNull()) {
$params[$paramName] = null;
} else {
if (!isset($context['not_normalizable_value_exceptions'])) {
$missingConstructorArguments[] = $constructorParameter->name;
continue;
}
$constructorParameterType = 'unknown';
$reflectionType = $constructorParameter->getType();
if ($reflectionType instanceof \ReflectionNamedType) {
$constructorParameterType = $reflectionType->getName();
}
$exception = NotNormalizableValueException::createForUnexpectedDataType(
sprintf('Failed to create object because the class misses the "%s" property.', $constructorParameter->name),
null,
[$constructorParameterType],
$attributeContext['deserialization_path'] ?? null,
true
);
$context['not_normalizable_value_exceptions'][] = $exception;
}
}
if ($missingConstructorArguments) {
throw new MissingConstructorArgumentsException(sprintf('Cannot create an instance of "%s" from serialized data because its constructor requires the following parameters to be present : "$%s".', $class, implode('", "$', $missingConstructorArguments)), 0, null, $missingConstructorArguments, $class);
}
if (!$constructor->isConstructor()) {
$instance = $constructor->invokeArgs(null, $params);
// do not set a parameter that has been set in the constructor
foreach ($unsetKeys as $key) {
unset($data[$key]);
}
return $instance;
}
try {
$instance = $reflectionClass->newInstanceArgs($params);
// do not set a parameter that has been set in the constructor
foreach ($unsetKeys as $key) {
unset($data[$key]);
}
return $instance;
} catch (\TypeError $e) {
if (!isset($context['not_normalizable_value_exceptions'])) {
throw $e;
}
return $reflectionClass->newInstanceWithoutConstructor();
}
}
unset($context['has_constructor']);
if (!$reflectionClass->isInstantiable()) {
throw NotNormalizableValueException::createForUnexpectedDataType(
sprintf('Failed to create object because the class "%s" is not instantiable.', $class),
$data,
['unknown'],
$context['deserialization_path'] ?? null
);
}
return new $class();
}
/**
* @internal
*/
protected function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameter, string $parameterName, mixed $parameterData, array $context, ?string $format = null): mixed
{
try {
if (($parameterType = $parameter->getType()) instanceof \ReflectionNamedType && !$parameterType->isBuiltin()) {
$parameterClass = $parameterType->getName();
new \ReflectionClass($parameterClass); // throws a \ReflectionException if the class doesn't exist
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));
}
$parameterData = $this->serializer->denormalize($parameterData, $parameterClass, $format, $this->createChildContext($context, $parameterName, $format));
}
} 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);
}
/**
* @internal
*/
protected function createChildContext(array $parentContext, string $attribute, ?string $format): array
{
if (isset($parentContext[self::ATTRIBUTES][$attribute])) {
$parentContext[self::ATTRIBUTES] = $parentContext[self::ATTRIBUTES][$attribute];
} else {
unset($parentContext[self::ATTRIBUTES]);
}
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" : ''));
}
}
}
final protected function applyCallbacks(mixed $value, object|string $object, string $attribute, ?string $format, array $context): mixed
{
/**
* @var callable|null
*/
$callback = $context[self::CALLBACKS][$attribute] ?? $this->defaultContext[self::CALLBACKS][$attribute] ?? null;
return $callback ? $callback($value, $object, $attribute, $format, $context) : $value;
}
/**
* Computes the normalization context merged with current one. Metadata always wins over global context, as more specific.
*
* @internal
*/
protected function getAttributeNormalizationContext(object $object, string $attribute, array $context): array
{
if (null === $metadata = $this->getAttributeMetadata($object, $attribute)) {
return $context;
}
return array_merge($context, $metadata->getNormalizationContextForGroups($this->getGroups($context)));
}
/**
* Computes the denormalization context merged with current one. Metadata always wins over global context, as more specific.
*
* @internal
*/
protected function getAttributeDenormalizationContext(string $class, string $attribute, array $context): array
{
$context['deserialization_path'] = ($context['deserialization_path'] ?? false) ? $context['deserialization_path'].'.'.$attribute : $attribute;
if (null === $metadata = $this->getAttributeMetadata($class, $attribute)) {
return $context;
}
return array_merge($context, $metadata->getDenormalizationContextForGroups($this->getGroups($context)));
}
/**
* @internal
*/
protected function getAttributeMetadata(object|string $objectOrClass, string $attribute): ?AttributeMetadataInterface
{
if (!$this->classMetadataFactory) {
return null;
}
return $this->classMetadataFactory->getMetadataFor($objectOrClass)->getAttributesMetadata()[$attribute] ?? null;
}
}

View File

@@ -0,0 +1,864 @@
<?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\PropertyAccess\Exception\InvalidArgumentException as PropertyAccessInvalidArgumentException;
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Serializer\Encoder\CsvEncoder;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Exception\ExtraAttributesException;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
use Symfony\Component\Serializer\Mapping\ClassMetadataInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
/**
* Base class for a normalizer dealing with objects.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
abstract class AbstractObjectNormalizer extends AbstractNormalizer
{
/**
* 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';
/**
* Flag to control whether uninitialized PHP>=7.4 typed class properties
* should be excluded when normalizing.
*/
public const SKIP_UNINITIALIZED_VALUES = 'skip_uninitialized_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';
/**
* Flag to control whether an empty object should be kept as an object (in
* JSON: {}) or converted to a list (in JSON: []).
*/
public const PRESERVE_EMPTY_OBJECTS = 'preserve_empty_objects';
private array $typesCache = [];
private array $attributesCache = [];
private readonly \Closure $objectClassResolver;
/**
* @var ClassDiscriminatorResolverInterface|null
*/
protected $classDiscriminatorResolver;
public function __construct(
?ClassMetadataFactoryInterface $classMetadataFactory = null,
?NameConverterInterface $nameConverter = null,
private ?PropertyTypeExtractorInterface $propertyTypeExtractor = null,
?ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null,
?callable $objectClassResolver = null,
array $defaultContext = [],
) {
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]);
if ($classMetadataFactory) {
$classDiscriminatorResolver ??= new ClassDiscriminatorFromClassMetadata($classMetadataFactory);
}
$this->classDiscriminatorResolver = $classDiscriminatorResolver;
$this->objectClassResolver = ($objectClassResolver ?? 'get_class')(...);
}
/**
* @param array $context
*
* @return bool
*/
public function supportsNormalization(mixed $data, ?string $format = null /* , array $context = [] */)
{
return \is_object($data) && !$data instanceof \Traversable;
}
/**
* @return array|string|int|float|bool|\ArrayObject|null
*/
public function normalize(mixed $object, ?string $format = null, array $context = [])
{
$context['_read_attributes'] = true;
if (!isset($context['cache_key'])) {
$context['cache_key'] = $this->getCacheKey($format, $context);
}
$this->validateCallbackContext($context);
if ($this->isCircularReference($object, $context)) {
return $this->handleCircularReference($object, $format, $context);
}
$data = [];
$stack = [];
$attributes = $this->getAttributes($object, $format, $context);
$class = ($this->objectClassResolver)($object);
$classMetadata = $this->classMetadataFactory?->getMetadataFor($class);
$attributesMetadata = $this->classMetadataFactory?->getMetadataFor($class)->getAttributesMetadata();
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 {
$maxDepthHandler = null;
}
foreach ($attributes as $attribute) {
$maxDepthReached = false;
if (null !== $attributesMetadata && ($maxDepthReached = $this->isMaxDepthReached($attributesMetadata, $class, $attribute, $context)) && !$maxDepthHandler) {
continue;
}
$attributeContext = $this->getAttributeNormalizationContext($object, $attribute, $context);
try {
$attributeValue = $attribute === $this->classDiscriminatorResolver?->getMappingForMappedObject($object)?->getTypeProperty()
? $this->classDiscriminatorResolver?->getTypeForMappedObject($object)
: $this->getAttributeValue($object, $attribute, $format, $attributeContext);
} catch (UninitializedPropertyException|\Error $e) {
if (($context[self::SKIP_UNINITIALIZED_VALUES] ?? $this->defaultContext[self::SKIP_UNINITIALIZED_VALUES] ?? true) && $this->isUninitializedValueError($e)) {
continue;
}
throw $e;
}
if ($maxDepthReached) {
$attributeValue = $maxDepthHandler($attributeValue, $object, $attribute, $format, $attributeContext);
}
$stack[$attribute] = $this->applyCallbacks($attributeValue, $object, $attribute, $format, $attributeContext);
}
foreach ($stack as $attribute => $attributeValue) {
$attributeContext = $this->getAttributeNormalizationContext($object, $attribute, $context);
if (null === $attributeValue || \is_scalar($attributeValue)) {
$data = $this->updateData($data, $attribute, $attributeValue, $class, $format, $attributeContext, $attributesMetadata, $classMetadata);
continue;
}
if (!$this->serializer instanceof NormalizerInterface) {
throw new LogicException(sprintf('Cannot normalize attribute "%s" because the injected serializer is not a normalizer.', $attribute));
}
$childContext = $this->createChildContext($attributeContext, $attribute, $format);
$data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $childContext), $class, $format, $attributeContext, $attributesMetadata, $classMetadata);
}
$preserveEmptyObjects = $context[self::PRESERVE_EMPTY_OBJECTS] ?? $this->defaultContext[self::PRESERVE_EMPTY_OBJECTS] ?? false;
if ($preserveEmptyObjects && !$data) {
return new \ArrayObject();
}
return $data;
}
/**
* @return object
*/
protected function instantiateObject(array &$data, string $class, array &$context, \ReflectionClass $reflectionClass, array|bool $allowedAttributes, ?string $format = null)
{
if ($class !== $mappedClass = $this->getMappedClass($data, $class, $context)) {
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.
*
* @return string[]
*/
protected function getAttributes(object $object, ?string $format, array $context): array
{
$class = ($this->objectClassResolver)($object);
$key = $class.'-'.$context['cache_key'];
if (isset($this->attributesCache[$key])) {
return $this->attributesCache[$key];
}
$allowedAttributes = $this->getAllowedAttributes($object, $context, true);
if (false !== $allowedAttributes) {
if ($context['cache_key']) {
$this->attributesCache[$key] = $allowedAttributes;
}
return $allowedAttributes;
}
$attributes = $this->extractAttributes($object, $format, $context);
if ($mapping = $this->classDiscriminatorResolver?->getMappingForMappedObject($object)) {
array_unshift($attributes, $mapping->getTypeProperty());
}
if ($context['cache_key'] && \stdClass::class !== $class) {
$this->attributesCache[$key] = $attributes;
}
return $attributes;
}
/**
* Extracts attributes to normalize from the class of the given object, format and context.
*
* @return string[]
*/
abstract protected function extractAttributes(object $object, ?string $format = null, array $context = []);
/**
* Gets the attribute value.
*
* @return mixed
*/
abstract protected function getAttributeValue(object $object, string $attribute, ?string $format = null, array $context = []);
/**
* @param array $context
*
* @return bool
*/
public function supportsDenormalization(mixed $data, string $type, ?string $format = null /* , array $context = [] */)
{
return class_exists($type) || (interface_exists($type, false) && null !== $this->classDiscriminatorResolver?->getMappingForClass($type));
}
/**
* @return mixed
*/
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = [])
{
$context['_read_attributes'] = false;
if (!isset($context['cache_key'])) {
$context['cache_key'] = $this->getCacheKey($format, $context);
}
$this->validateCallbackContext($context);
if (null === $data && isset($context['value_type']) && $context['value_type'] instanceof Type && $context['value_type']->isNullable()) {
return null;
}
if (XmlEncoder::FORMAT === $format && !\is_array($data)) {
$data = ['#' => $data];
}
$allowedAttributes = $this->getAllowedAttributes($type, $context, true);
$normalizedData = $this->prepareForDenormalization($data);
$extraAttributes = [];
$mappedClass = $this->getMappedClass($normalizedData, $type, $context);
$nestedAttributes = $this->getNestedAttributes($mappedClass);
$nestedData = $originalNestedData = [];
$propertyAccessor = PropertyAccess::createPropertyAccessor();
foreach ($nestedAttributes as $property => $serializedPath) {
if (null === $value = $propertyAccessor->getValue($normalizedData, $serializedPath)) {
continue;
}
$convertedProperty = $this->nameConverter ? $this->nameConverter->normalize($property, $mappedClass, $format, $context) : $property;
$nestedData[$convertedProperty] = $value;
$originalNestedData[$property] = $value;
$normalizedData = $this->removeNestedValue($serializedPath->getElements(), $normalizedData);
}
$normalizedData = $nestedData + $normalizedData;
$object = $this->instantiateObject($normalizedData, $mappedClass, $context, new \ReflectionClass($mappedClass), $allowedAttributes, $format);
$resolvedClass = ($this->objectClassResolver)($object);
foreach ($normalizedData as $attribute => $value) {
if ($this->nameConverter) {
$notConverted = $attribute;
$attribute = $this->nameConverter->denormalize($attribute, $resolvedClass, $format, $context);
if (isset($nestedData[$notConverted]) && !isset($originalNestedData[$attribute])) {
throw new LogicException(sprintf('Duplicate values for key "%s" found. One value is set via the SerializedPath attribute: "%s", the other one is set via the SerializedName attribute: "%s".', $notConverted, implode('->', $nestedAttributes[$notConverted]->getElements()), $attribute));
}
}
$attributeContext = $this->getAttributeDenormalizationContext($resolvedClass, $attribute, $context);
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;
}
if ($attributeContext[self::DEEP_OBJECT_TO_POPULATE] ?? $this->defaultContext[self::DEEP_OBJECT_TO_POPULATE] ?? false) {
$discriminatorMapping = $this->classDiscriminatorResolver?->getMappingForMappedObject($object);
try {
$attributeContext[self::OBJECT_TO_POPULATE] = $attribute === $discriminatorMapping?->getTypeProperty()
? $discriminatorMapping
: $this->getAttributeValue($object, $attribute, $format, $attributeContext);
} catch (NoSuchPropertyException) {
} catch (UninitializedPropertyException|\Error $e) {
if (!(($context[self::SKIP_UNINITIALIZED_VALUES] ?? $this->defaultContext[self::SKIP_UNINITIALIZED_VALUES] ?? true) && $this->isUninitializedValueError($e))) {
throw $e;
}
}
}
$types = $this->getTypes($resolvedClass, $attribute);
if (null !== $types) {
try {
$value = $this->validateAndDenormalize($types, $resolvedClass, $attribute, $value, $format, $attributeContext);
} catch (NotNormalizableValueException $exception) {
if (isset($context['not_normalizable_value_exceptions'])) {
$context['not_normalizable_value_exceptions'][] = $exception;
continue;
}
throw $exception;
}
}
$value = $this->applyCallbacks($value, $resolvedClass, $attribute, $format, $attributeContext);
try {
$this->setAttributeValue($object, $attribute, $value, $format, $attributeContext);
} catch (PropertyAccessInvalidArgumentException $e) {
$exception = NotNormalizableValueException::createForUnexpectedDataType(
sprintf('Failed to denormalize attribute "%s" value for class "%s": '.$e->getMessage(), $attribute, $type),
$data,
['unknown'],
$attributeContext['deserialization_path'] ?? null,
false,
$e->getCode(),
$e
);
if (isset($context['not_normalizable_value_exceptions'])) {
$context['not_normalizable_value_exceptions'][] = $exception;
continue;
}
throw $exception;
}
}
if ($extraAttributes) {
throw new ExtraAttributesException($extraAttributes);
}
return $object;
}
/**
* @return void
*/
abstract protected function setAttributeValue(object $object, string $attribute, mixed $value, ?string $format = null, array $context = []);
/**
* Validates the submitted data and denormalizes it.
*
* @param Type[] $types
*
* @throws NotNormalizableValueException
* @throws ExtraAttributesException
* @throws MissingConstructorArgumentsException
* @throws LogicException
*/
private function validateAndDenormalize(array $types, string $currentClass, string $attribute, mixed $data, ?string $format, array $context): mixed
{
$expectedTypes = [];
$isUnionType = \count($types) > 1;
$e = null;
$extraAttributesException = null;
$missingConstructorArgumentsException = null;
$isNullable = false;
foreach ($types as $type) {
if (null === $data && $type->isNullable()) {
return null;
}
$collectionValueType = $type->isCollection() ? $type->getCollectionValueTypes()[0] ?? null : null;
// Fix a collection that contains the only one element
// This is special to xml format only
if ('xml' === $format && null !== $collectionValueType && (!\is_array($data) || !\is_int(key($data)))) {
$data = [$data];
}
// 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 {
// In XML and CSV all basic datatypes are represented as strings, it is e.g. not possible to determine,
// if a value is meant to be a string, float, int or a boolean value from the serialized representation.
// That's why we have to transform the values, if one of these non-string basic datatypes is expected.
$builtinType = $type->getBuiltinType();
if (\is_string($data) && (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format)) {
if ('' === $data) {
if (Type::BUILTIN_TYPE_ARRAY === $builtinType) {
return [];
}
if (Type::BUILTIN_TYPE_STRING === $builtinType) {
return '';
}
// Don't return null yet because Object-types that come first may accept empty-string too
$isNullable = $isNullable ?: $type->isNullable();
}
switch ($builtinType) {
case Type::BUILTIN_TYPE_BOOL:
// according to https://www.w3.org/TR/xmlschema-2/#boolean, valid representations are "false", "true", "0" and "1"
if ('false' === $data || '0' === $data) {
$data = false;
} elseif ('true' === $data || '1' === $data) {
$data = true;
} else {
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).', $attribute, $currentClass, $data), $data, [Type::BUILTIN_TYPE_BOOL], $context['deserialization_path'] ?? null);
}
break;
case Type::BUILTIN_TYPE_INT:
if (ctype_digit(isset($data[0]) && '-' === $data[0] ? substr($data, 1) : $data)) {
$data = (int) $data;
} else {
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $currentClass, $data), $data, [Type::BUILTIN_TYPE_INT], $context['deserialization_path'] ?? null);
}
break;
case Type::BUILTIN_TYPE_FLOAT:
if (is_numeric($data)) {
return (float) $data;
}
return match ($data) {
'NaN' => \NAN,
'INF' => \INF,
'-INF' => -\INF,
default => throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).', $attribute, $currentClass, $data), $data, [Type::BUILTIN_TYPE_FLOAT], $context['deserialization_path'] ?? null),
};
}
}
if (null !== $collectionValueType && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) {
$builtinType = Type::BUILTIN_TYPE_OBJECT;
$class = $collectionValueType->getClassName().'[]';
if (\count($collectionKeyType = $type->getCollectionKeyTypes()) > 0) {
$context['key_type'] = \count($collectionKeyType) > 1 ? $collectionKeyType : $collectionKeyType[0];
}
$context['value_type'] = $collectionValueType;
} elseif ($type->isCollection() && \count($collectionValueType = $type->getCollectionValueTypes()) > 0 && Type::BUILTIN_TYPE_ARRAY === $collectionValueType[0]->getBuiltinType()) {
// get inner type for any nested array
[$innerType] = $collectionValueType;
// note that it will break for any other builtinType
$dimensions = '[]';
while (\count($innerType->getCollectionValueTypes()) > 0 && Type::BUILTIN_TYPE_ARRAY === $innerType->getBuiltinType()) {
$dimensions .= '[]';
[$innerType] = $innerType->getCollectionValueTypes();
}
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();
}
$expectedTypes[Type::BUILTIN_TYPE_OBJECT === $builtinType && $class ? $class : $builtinType] = true;
if (Type::BUILTIN_TYPE_OBJECT === $builtinType && null !== $class) {
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);
}
}
// 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;
}
if ((Type::BUILTIN_TYPE_FALSE === $builtinType && false === $data) || (Type::BUILTIN_TYPE_TRUE === $builtinType && true === $data)) {
return $data;
}
switch ($builtinType) {
case Type::BUILTIN_TYPE_ARRAY:
case Type::BUILTIN_TYPE_BOOL:
case Type::BUILTIN_TYPE_CALLABLE:
case Type::BUILTIN_TYPE_FLOAT:
case Type::BUILTIN_TYPE_INT:
case Type::BUILTIN_TYPE_ITERABLE:
case Type::BUILTIN_TYPE_NULL:
case Type::BUILTIN_TYPE_OBJECT:
case Type::BUILTIN_TYPE_RESOURCE:
case Type::BUILTIN_TYPE_STRING:
if (('is_'.$builtinType)($data)) {
return $data;
}
break;
}
} catch (NotNormalizableValueException|InvalidArgumentException $e) {
if (!$isUnionType && !$isNullable) {
throw $e;
}
} catch (ExtraAttributesException $e) {
if (!$isUnionType && !$isNullable) {
throw $e;
}
$extraAttributesException ??= $e;
} catch (MissingConstructorArgumentsException $e) {
if (!$isUnionType && !$isNullable) {
throw $e;
}
$missingConstructorArgumentsException ??= $e;
}
}
if ($isNullable) {
return null;
}
if ($extraAttributesException) {
throw $extraAttributesException;
}
if ($missingConstructorArgumentsException) {
throw $missingConstructorArgumentsException;
}
if (!$isUnionType && $e) {
throw $e;
}
if ($context[self::DISABLE_TYPE_ENFORCEMENT] ?? $this->defaultContext[self::DISABLE_TYPE_ENFORCEMENT] ?? false) {
return $data;
}
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be one of "%s" ("%s" given).', $attribute, $currentClass, implode('", "', array_keys($expectedTypes)), get_debug_type($data)), $data, array_keys($expectedTypes), $context['deserialization_path'] ?? $attribute);
}
/**
* @internal
*/
protected function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameter, string $parameterName, mixed $parameterData, array $context, ?string $format = null): mixed
{
if ($parameter->isVariadic() || null === $this->propertyTypeExtractor || null === $types = $this->getTypes($class->getName(), $parameterName)) {
return parent::denormalizeParameter($class, $parameter, $parameterName, $parameterData, $context, $format);
}
$parameterData = $this->validateAndDenormalize($types, $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 ($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.
*/
private function updateData(array $data, string $attribute, mixed $attributeValue, string $class, ?string $format, array $context, ?array $attributesMetadata, ?ClassMetadataInterface $classMetadata): array
{
if (null === $attributeValue && ($context[self::SKIP_NULL_VALUES] ?? $this->defaultContext[self::SKIP_NULL_VALUES] ?? false)) {
return $data;
}
if (null !== $classMetadata && null !== $serializedPath = ($attributesMetadata[$attribute] ?? null)?->getSerializedPath()) {
$propertyAccessor = PropertyAccess::createPropertyAccessor();
if ($propertyAccessor->isReadable($data, $serializedPath) && null !== $propertyAccessor->getValue($data, $serializedPath)) {
throw new LogicException(sprintf('The element you are trying to set is already populated: "%s".', (string) $serializedPath));
}
$propertyAccessor->setValue($data, $serializedPath, $attributeValue);
return $data;
}
if ($this->nameConverter) {
$attribute = $this->nameConverter->normalize($attribute, $class, $format, $context);
}
$data[$attribute] = $attributeValue;
return $data;
}
/**
* Is the max depth reached for the given attribute?
*
* @param AttributeMetadataInterface[] $attributesMetadata
*/
private function isMaxDepthReached(array $attributesMetadata, string $class, string $attribute, array &$context): bool
{
if (!($enableMaxDepth = $context[self::ENABLE_MAX_DEPTH] ?? $this->defaultContext[self::ENABLE_MAX_DEPTH] ?? false)
|| !isset($attributesMetadata[$attribute]) || null === $maxDepth = $attributesMetadata[$attribute]?->getMaxDepth()
) {
return false;
}
$key = sprintf(self::DEPTH_KEY_PATTERN, $class, $attribute);
if (!isset($context[$key])) {
$context[$key] = 1;
return false;
}
if ($context[$key] === $maxDepth) {
return true;
}
++$context[$key];
return false;
}
/**
* Overwritten to update the cache key for the child.
*
* We must not mix up the attribute cache between parent and children.
*
* @internal
*/
protected function createChildContext(array $parentContext, string $attribute, ?string $format): array
{
$context = parent::createChildContext($parentContext, $attribute, $format);
if ($context['cache_key'] ?? false) {
$context['cache_key'] .= '-'.$attribute;
} elseif (false !== ($context['cache_key'] ?? null)) {
$context['cache_key'] = $this->getCacheKey($format, $context);
}
return $context;
}
/**
* Builds the cache key for the attributes cache.
*
* The key must be different for every option in the context that could change which attributes should be handled.
*/
private function getCacheKey(?string $format, array $context): bool|string
{
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 hash('xxh128', $format.serialize([
'context' => $context,
'ignored' => $context[self::IGNORED_ATTRIBUTES] ?? $this->defaultContext[self::IGNORED_ATTRIBUTES],
]));
} catch (\Exception) {
// 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|UninitializedPropertyException $e): bool
{
return $e instanceof UninitializedPropertyException
|| str_starts_with($e->getMessage(), 'Typed property')
&& str_ends_with($e->getMessage(), 'must not be accessed before initialization');
}
/**
* Returns all attributes with a SerializedPath attribute and the respective path.
*/
private function getNestedAttributes(string $class): array
{
if (!$this->classMetadataFactory?->hasMetadataFor($class)) {
return [];
}
$properties = [];
$serializedPaths = [];
$classMetadata = $this->classMetadataFactory->getMetadataFor($class);
foreach ($classMetadata->getAttributesMetadata() as $name => $metadata) {
if (!$serializedPath = $metadata->getSerializedPath()) {
continue;
}
$pathIdentifier = implode(',', $serializedPath->getElements());
if (isset($serializedPaths[$pathIdentifier])) {
throw new LogicException(sprintf('Duplicate serialized path: "%s" used for properties "%s" and "%s".', $pathIdentifier, $serializedPaths[$pathIdentifier], $name));
}
$serializedPaths[$pathIdentifier] = $name;
$properties[$name] = $serializedPath;
}
return $properties;
}
private function removeNestedValue(array $path, array $data): array
{
$element = array_shift($path);
if (!$path || !$data[$element] = $this->removeNestedValue($path, $data[$element])) {
unset($data[$element]);
}
return $data;
}
/**
* @return class-string
*/
private function getMappedClass(array $data, string $class, array $context): string
{
if (null !== $object = $this->extractObjectToPopulate($class, $context, self::OBJECT_TO_POPULATE)) {
return $object::class;
}
if (!$mapping = $this->classDiscriminatorResolver?->getMappingForClass($class)) {
return $class;
}
if (null === $type = $data[$mapping->getTypeProperty()] ?? null) {
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Type property "%s" not found for the abstract object "%s".', $mapping->getTypeProperty(), $class), null, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), false);
}
if (null === $mappedClass = $mapping->getClassForType($type)) {
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type "%s" is not a valid value.', $type), $type, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), true);
}
return $mappedClass;
}
}

View File

@@ -0,0 +1,114 @@
<?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\PropertyInfo\Type;
use Symfony\Component\Serializer\Exception\BadMethodCallException;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
/**
* Denormalizes arrays of objects.
*
* @author Alexander M. Turek <me@derrabus.de>
*
* @final
*/
class ArrayDenormalizer implements ContextAwareDenormalizerInterface, DenormalizerAwareInterface, CacheableSupportsMethodInterface
{
use DenormalizerAwareTrait;
public function setDenormalizer(DenormalizerInterface $denormalizer): void
{
if (!method_exists($denormalizer, 'getSupportedTypes')) {
trigger_deprecation('symfony/serializer', '6.3', 'Not implementing the "DenormalizerInterface::getSupportedTypes()" in "%s" is deprecated.', get_debug_type($denormalizer));
}
$this->denormalizer = $denormalizer;
}
public function getSupportedTypes(?string $format): array
{
return ['object' => null, '*' => false];
}
/**
* @throws NotNormalizableValueException
*/
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): array
{
if (null === $this->denormalizer) {
throw new BadMethodCallException('Please set a denormalizer before calling denormalize()!');
}
if (!\is_array($data)) {
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Data expected to be "%s", "%s" given.', $type, get_debug_type($data)), $data, [Type::BUILTIN_TYPE_ARRAY], $context['deserialization_path'] ?? null);
}
if (!str_ends_with($type, '[]')) {
throw new InvalidArgumentException('Unsupported class: '.$type);
}
$type = substr($type, 0, -2);
$builtinTypes = array_map(static function (Type $keyType) {
return $keyType->getBuiltinType();
}, \is_array($keyType = $context['key_type'] ?? []) ? $keyType : [$keyType]);
foreach ($data as $key => $value) {
$subContext = $context;
$subContext['deserialization_path'] = ($context['deserialization_path'] ?? false) ? sprintf('%s[%s]', $context['deserialization_path'], $key) : "[$key]";
$this->validateKeyType($builtinTypes, $key, $subContext['deserialization_path']);
$data[$key] = $this->denormalizer->denormalize($value, $type, $format, $subContext);
}
return $data;
}
public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
{
if (null === $this->denormalizer) {
throw new BadMethodCallException(sprintf('The nested denormalizer needs to be set to allow "%s()" to be used.', __METHOD__));
}
return str_ends_with($type, '[]')
&& $this->denormalizer->supportsDenormalization($data, substr($type, 0, -2), $format, $context);
}
/**
* @deprecated since Symfony 6.3, use "getSupportedTypes()" instead
*/
public function hasCacheableSupportsMethod(): bool
{
trigger_deprecation('symfony/serializer', '6.3', 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', __METHOD__);
return $this->denormalizer instanceof CacheableSupportsMethodInterface && $this->denormalizer->hasCacheableSupportsMethod();
}
/**
* @param mixed $key
*/
private function validateKeyType(array $builtinTypes, $key, string $path): void
{
if (!$builtinTypes) {
return;
}
foreach ($builtinTypes as $builtinType) {
if (('is_'.$builtinType)($key)) {
return;
}
}
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the key "%s" must be "%s" ("%s" given).', $key, implode('", "', $builtinTypes), get_debug_type($key)), $key, $builtinTypes, $path, true);
}
}

View File

@@ -0,0 +1,101 @@
<?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\PropertyInfo\Type;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
/**
* Normalizes a {@see \BackedEnum} enumeration to a string or an integer.
*
* @author Alexandre Daubois <alex.daubois@gmail.com>
*/
final class BackedEnumNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface
{
/**
* If true, will denormalize any invalid value into null.
*/
public const ALLOW_INVALID_VALUES = 'allow_invalid_values';
public function getSupportedTypes(?string $format): array
{
return [
\BackedEnum::class => true,
];
}
public function normalize(mixed $object, ?string $format = null, array $context = []): int|string
{
if (!$object instanceof \BackedEnum) {
throw new InvalidArgumentException('The data must belong to a backed enumeration.');
}
return $object->value;
}
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
return $data instanceof \BackedEnum;
}
/**
* @throws NotNormalizableValueException
*/
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed
{
if (!is_subclass_of($type, \BackedEnum::class)) {
throw new InvalidArgumentException('The data must belong to a backed enumeration.');
}
if ($context[self::ALLOW_INVALID_VALUES] ?? false) {
if (null === $data || (!\is_int($data) && !\is_string($data))) {
return null;
}
try {
return $type::tryFrom($data);
} catch (\TypeError) {
return null;
}
}
if (!\is_int($data) && !\is_string($data)) {
throw NotNormalizableValueException::createForUnexpectedDataType('The data is neither an integer nor a string, you should pass an integer or a string that can be parsed as an enumeration case of type '.$type.'.', $data, [Type::BUILTIN_TYPE_INT, Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true);
}
try {
return $type::from($data);
} catch (\ValueError $e) {
if (isset($context['has_constructor'])) {
throw new InvalidArgumentException('The data must belong to a backed enumeration of type '.$type, 0, $e);
}
throw NotNormalizableValueException::createForUnexpectedDataType('The data must belong to a backed enumeration of type '.$type, $data, [$type], $context['deserialization_path'] ?? null, true, 0, $e);
}
}
public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
{
return is_subclass_of($type, \BackedEnum::class);
}
/**
* @deprecated since Symfony 6.3, use "getSupportedTypes()" instead
*/
public function hasCacheableSupportsMethod(): bool
{
trigger_deprecation('symfony/serializer', '6.3', 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', __METHOD__);
return true;
}
}

View File

@@ -0,0 +1,28 @@
<?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;
/**
* Marker interface for normalizers and denormalizers that use
* only the type and the format in their supports*() methods.
*
* By implementing this interface, the return value of the
* supports*() methods will be cached by type and format.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @deprecated since Symfony 6.3, implement "getSupportedTypes(?string $format)" instead
*/
interface CacheableSupportsMethodInterface
{
public function hasCacheableSupportsMethod(): bool;
}

View File

@@ -0,0 +1,128 @@
<?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\NameConverter\NameConverterInterface;
use Symfony\Component\Validator\ConstraintViolationListInterface;
/**
* A normalizer that normalizes a ConstraintViolationListInterface instance.
*
* This Normalizer implements RFC7807 {@link https://tools.ietf.org/html/rfc7807}.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @final since Symfony 6.3
*/
class ConstraintViolationListNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface
{
public const INSTANCE = 'instance';
public const STATUS = 'status';
public const TITLE = 'title';
public const TYPE = 'type';
public const PAYLOAD_FIELDS = 'payload_fields';
public function __construct(
private readonly array $defaultContext = [],
private readonly ?NameConverterInterface $nameConverter = null,
) {
}
public function getSupportedTypes(?string $format): array
{
return [
ConstraintViolationListInterface::class => __CLASS__ === static::class || $this->hasCacheableSupportsMethod(),
];
}
public function normalize(mixed $object, ?string $format = null, array $context = []): array
{
if (\array_key_exists(self::PAYLOAD_FIELDS, $context)) {
$payloadFieldsToSerialize = $context[self::PAYLOAD_FIELDS];
} elseif (\array_key_exists(self::PAYLOAD_FIELDS, $this->defaultContext)) {
$payloadFieldsToSerialize = $this->defaultContext[self::PAYLOAD_FIELDS];
} else {
$payloadFieldsToSerialize = [];
}
if (\is_array($payloadFieldsToSerialize) && [] !== $payloadFieldsToSerialize) {
$payloadFieldsToSerialize = array_flip($payloadFieldsToSerialize);
}
$violations = [];
$messages = [];
foreach ($object as $violation) {
$propertyPath = $this->nameConverter ? $this->nameConverter->normalize($violation->getPropertyPath(), null, $format, $context) : $violation->getPropertyPath();
$violationEntry = [
'propertyPath' => $propertyPath,
'title' => $violation->getMessage(),
'template' => $violation->getMessageTemplate(),
'parameters' => $violation->getParameters(),
];
if (null !== $code = $violation->getCode()) {
$violationEntry['type'] = sprintf('urn:uuid:%s', $code);
}
$constraint = $violation->getConstraint();
if (
[] !== $payloadFieldsToSerialize
&& $constraint
&& $constraint->payload
// If some or all payload fields are whitelisted, add them
&& $payloadFields = null === $payloadFieldsToSerialize || true === $payloadFieldsToSerialize ? $constraint->payload : array_intersect_key($constraint->payload, $payloadFieldsToSerialize)
) {
$violationEntry['payload'] = $payloadFields;
}
$violations[] = $violationEntry;
$prefix = $propertyPath ? sprintf('%s: ', $propertyPath) : '';
$messages[] = $prefix.$violation->getMessage();
}
$result = [
'type' => $context[self::TYPE] ?? $this->defaultContext[self::TYPE] ?? 'https://symfony.com/errors/validation',
'title' => $context[self::TITLE] ?? $this->defaultContext[self::TITLE] ?? 'Validation Failed',
];
if (null !== $status = ($context[self::STATUS] ?? $this->defaultContext[self::STATUS] ?? null)) {
$result['status'] = $status;
}
if ($messages) {
$result['detail'] = implode("\n", $messages);
}
if (null !== $instance = ($context[self::INSTANCE] ?? $this->defaultContext[self::INSTANCE] ?? null)) {
$result['instance'] = $instance;
}
return $result + ['violations' => $violations];
}
/**
* @param array $context
*/
public function supportsNormalization(mixed $data, ?string $format = null /* , array $context = [] */): bool
{
return $data instanceof ConstraintViolationListInterface;
}
/**
* @deprecated since Symfony 6.3, use "getSupportedTypes()" instead
*/
public function hasCacheableSupportsMethod(): bool
{
trigger_deprecation('symfony/serializer', '6.3', 'The "%s()" method is deprecated, implement "%s::getSupportedTypes()" instead.', __METHOD__, get_debug_type($this));
return __CLASS__ === static::class;
}
}

View File

@@ -0,0 +1,27 @@
<?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;
/**
* Adds the support of an extra $context parameter for the supportsDenormalization method.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @deprecated since symfony/serializer 6.1, use DenormalizerInterface instead
*/
interface ContextAwareDenormalizerInterface extends DenormalizerInterface
{
/**
* @param array $context options that denormalizers have access to
*/
public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool;
}

View File

@@ -0,0 +1,27 @@
<?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;
/**
* Adds the support of an extra $context parameter for the supportsNormalization method.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @deprecated since symfony/serializer 6.1, use NormalizerInterface instead
*/
interface ContextAwareNormalizerInterface extends NormalizerInterface
{
/**
* @param array $context options that normalizers have access to
*/
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool;
}

View File

@@ -0,0 +1,82 @@
<?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;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @final since Symfony 6.3
*/
class CustomNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface, CacheableSupportsMethodInterface
{
use ObjectToPopulateTrait;
use SerializerAwareTrait;
public function getSupportedTypes(?string $format): array
{
return [
NormalizableInterface::class => __CLASS__ === static::class || $this->hasCacheableSupportsMethod(),
DenormalizableInterface::class => __CLASS__ === static::class || $this->hasCacheableSupportsMethod(),
];
}
public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
{
return $object->normalize($this->serializer, $format, $context);
}
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed
{
$object = $this->extractObjectToPopulate($type, $context) ?? new $type();
$object->denormalize($this->serializer, $data, $format, $context);
return $object;
}
/**
* Checks if the given class implements the NormalizableInterface.
*
* @param mixed $data Data to normalize
* @param string|null $format The format being (de-)serialized from or into
* @param array $context
*/
public function supportsNormalization(mixed $data, ?string $format = null /* , array $context = [] */): bool
{
return $data instanceof NormalizableInterface;
}
/**
* Checks if the given class implements the DenormalizableInterface.
*
* @param mixed $data Data to denormalize from
* @param string $type The class to which the data should be denormalized
* @param string|null $format The format being deserialized from
* @param array $context
*/
public function supportsDenormalization(mixed $data, string $type, ?string $format = null /* , array $context = [] */): bool
{
return is_subclass_of($type, DenormalizableInterface::class);
}
/**
* @deprecated since Symfony 6.3, use "getSupportedTypes()" instead
*/
public function hasCacheableSupportsMethod(): bool
{
trigger_deprecation('symfony/serializer', '6.3', 'The "%s()" method is deprecated, implement "%s::getSupportedTypes()" instead.', __METHOD__, get_debug_type($this));
return __CLASS__ === static::class;
}
}

View File

@@ -0,0 +1,164 @@
<?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\HttpFoundation\File\File;
use Symfony\Component\Mime\MimeTypeGuesserInterface;
use Symfony\Component\Mime\MimeTypes;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
/**
* Normalizes an {@see \SplFileInfo} object to a data URI.
* Denormalizes a data URI to a {@see \SplFileObject} object.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @final since Symfony 6.3
*/
class DataUriNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface
{
private const SUPPORTED_TYPES = [
\SplFileInfo::class => true,
\SplFileObject::class => true,
File::class => true,
];
private readonly ?MimeTypeGuesserInterface $mimeTypeGuesser;
public function __construct(?MimeTypeGuesserInterface $mimeTypeGuesser = null)
{
if (!$mimeTypeGuesser && class_exists(MimeTypes::class)) {
$mimeTypeGuesser = MimeTypes::getDefault();
}
$this->mimeTypeGuesser = $mimeTypeGuesser;
}
public function getSupportedTypes(?string $format): array
{
$isCacheable = __CLASS__ === static::class || $this->hasCacheableSupportsMethod();
return [
\SplFileInfo::class => $isCacheable,
\SplFileObject::class => $isCacheable,
File::class => $isCacheable,
];
}
public function normalize(mixed $object, ?string $format = null, array $context = []): string
{
if (!$object instanceof \SplFileInfo) {
throw new InvalidArgumentException('The object must be an instance of "\SplFileInfo".');
}
$mimeType = $this->getMimeType($object);
$splFileObject = $this->extractSplFileObject($object);
$data = '';
$splFileObject->rewind();
while (!$splFileObject->eof()) {
$data .= $splFileObject->fgets();
}
if ('text' === explode('/', $mimeType, 2)[0]) {
return sprintf('data:%s,%s', $mimeType, rawurlencode($data));
}
return sprintf('data:%s;base64,%s', $mimeType, base64_encode($data));
}
/**
* @param array $context
*/
public function supportsNormalization(mixed $data, ?string $format = null /* , array $context = [] */): bool
{
return $data instanceof \SplFileInfo;
}
/**
* Regex adapted from Brian Grinstead code.
*
* @see https://gist.github.com/bgrins/6194623
*
* @throws InvalidArgumentException
* @throws NotNormalizableValueException
*/
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): \SplFileInfo
{
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 NotNormalizableValueException::createForUnexpectedDataType('The provided "data:" URI is not valid.', $data, ['string'], $context['deserialization_path'] ?? null, true);
}
try {
switch ($type) {
case File::class:
if (!class_exists(File::class)) {
throw new InvalidArgumentException(sprintf('Cannot denormalize to a "%s" without the HttpFoundation component installed. Try running "composer require symfony/http-foundation".', File::class));
}
return new File($data, false);
case 'SplFileObject':
case 'SplFileInfo':
return new \SplFileObject($data);
}
} catch (\RuntimeException $exception) {
throw NotNormalizableValueException::createForUnexpectedDataType($exception->getMessage(), $data, ['string'], $context['deserialization_path'] ?? null, false, $exception->getCode(), $exception);
}
throw new InvalidArgumentException(sprintf('The class parameter "%s" is not supported. It must be one of "SplFileInfo", "SplFileObject" or "Symfony\Component\HttpFoundation\File\File".', $type));
}
/**
* @param array $context
*/
public function supportsDenormalization(mixed $data, string $type, ?string $format = null /* , array $context = [] */): bool
{
return isset(self::SUPPORTED_TYPES[$type]);
}
/**
* @deprecated since Symfony 6.3, use "getSupportedTypes()" instead
*/
public function hasCacheableSupportsMethod(): bool
{
trigger_deprecation('symfony/serializer', '6.3', 'The "%s()" method is deprecated, implement "%s::getSupportedTypes()" instead.', __METHOD__, get_debug_type($this));
return __CLASS__ === static::class;
}
/**
* Gets the mime type of the object. Defaults to application/octet-stream.
*/
private function getMimeType(\SplFileInfo $object): string
{
if ($object instanceof File) {
return $object->getMimeType();
}
return $this->mimeTypeGuesser?->guessMimeType($object->getPathname()) ?: 'application/octet-stream';
}
/**
* Returns the \SplFileObject instance associated with the given \SplFileInfo instance.
*/
private function extractSplFileObject(\SplFileInfo $object): \SplFileObject
{
if ($object instanceof \SplFileObject) {
return $object;
}
return $object->openFile();
}
}

View File

@@ -0,0 +1,136 @@
<?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\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
/**
* Normalizes an instance of {@see \DateInterval} to an interval string.
* Denormalizes an interval string to an instance of {@see \DateInterval}.
*
* @author Jérôme Parmentier <jerome@prmntr.me>
*
* @final since Symfony 6.3
*/
class DateIntervalNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface
{
public const FORMAT_KEY = 'dateinterval_format';
private array $defaultContext = [
self::FORMAT_KEY => '%rP%yY%mM%dDT%hH%iM%sS',
];
public function __construct(array $defaultContext = [])
{
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
}
public function getSupportedTypes(?string $format): array
{
return [
\DateInterval::class => __CLASS__ === static::class || $this->hasCacheableSupportsMethod(),
];
}
/**
* @throws InvalidArgumentException
*/
public function normalize(mixed $object, ?string $format = null, array $context = []): string
{
if (!$object instanceof \DateInterval) {
throw new InvalidArgumentException('The object must be an instance of "\DateInterval".');
}
return $object->format($context[self::FORMAT_KEY] ?? $this->defaultContext[self::FORMAT_KEY]);
}
/**
* @param array $context
*/
public function supportsNormalization(mixed $data, ?string $format = null /* , array $context = [] */): bool
{
return $data instanceof \DateInterval;
}
/**
* @deprecated since Symfony 6.3, use "getSupportedTypes()" instead
*/
public function hasCacheableSupportsMethod(): bool
{
trigger_deprecation('symfony/serializer', '6.3', 'The "%s()" method is deprecated, implement "%s::getSupportedTypes()" instead.', __METHOD__, get_debug_type($this));
return __CLASS__ === static::class;
}
/**
* @throws NotNormalizableValueException
*/
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): \DateInterval
{
if (!\is_string($data)) {
throw NotNormalizableValueException::createForUnexpectedDataType('Data expected to be a string.', $data, ['string'], $context['deserialization_path'] ?? null, true);
}
if (!$this->isISO8601($data)) {
throw NotNormalizableValueException::createForUnexpectedDataType('Expected a valid ISO 8601 interval string.', $data, ['string'], $context['deserialization_path'] ?? null, true);
}
$dateIntervalFormat = $context[self::FORMAT_KEY] ?? $this->defaultContext[self::FORMAT_KEY];
$signPattern = '';
switch (substr($dateIntervalFormat, 0, 2)) {
case '%R':
$signPattern = '[-+]';
$dateIntervalFormat = substr($dateIntervalFormat, 2);
break;
case '%r':
$signPattern = '-?';
$dateIntervalFormat = substr($dateIntervalFormat, 2);
break;
}
$valuePattern = '/^'.$signPattern.preg_replace('/%([yYmMdDhHiIsSwW])(\w)/', '(?:(?P<$1>\d+)$2)?', preg_replace('/(T.*)$/', '($1)?', $dateIntervalFormat)).'$/';
if (!preg_match($valuePattern, $data)) {
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Value "%s" contains intervals not accepted by format "%s".', $data, $dateIntervalFormat), $data, ['string'], $context['deserialization_path'] ?? null, false);
}
try {
if ('-' === $data[0]) {
$interval = new \DateInterval(substr($data, 1));
$interval->invert = 1;
return $interval;
}
if ('+' === $data[0]) {
return new \DateInterval(substr($data, 1));
}
return new \DateInterval($data);
} catch (\Exception $e) {
throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $data, ['string'], $context['deserialization_path'] ?? null, false, $e->getCode(), $e);
}
}
/**
* @param array $context
*/
public function supportsDenormalization(mixed $data, string $type, ?string $format = null /* , array $context = [] */): bool
{
return \DateInterval::class === $type;
}
private function isISO8601(string $string): bool
{
return preg_match('/^[\-+]?P(?=\w*(?:\d|%\w))(?:\d+Y|%[yY]Y)?(?:\d+M|%[mM]M)?(?:\d+W|%[wW]W)?(?:\d+D|%[dD]D)?(?:T(?:\d+H|[hH]H)?(?:\d+M|[iI]M)?(?:\d+S|[sS]S)?)?$/', $string);
}
}

View File

@@ -0,0 +1,185 @@
<?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\PropertyInfo\Type;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
/**
* Normalizes an object implementing the {@see \DateTimeInterface} to a date string.
* Denormalizes a date string to an instance of {@see \DateTime} or {@see \DateTimeImmutable}.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @final since Symfony 6.3
*/
class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface
{
public const FORMAT_KEY = 'datetime_format';
public const TIMEZONE_KEY = 'datetime_timezone';
private array $defaultContext = [
self::FORMAT_KEY => \DateTimeInterface::RFC3339,
self::TIMEZONE_KEY => null,
];
private const SUPPORTED_TYPES = [
\DateTimeInterface::class => true,
\DateTimeImmutable::class => true,
\DateTime::class => true,
];
public function __construct(array $defaultContext = [])
{
$this->setDefaultContext($defaultContext);
}
public function setDefaultContext(array $defaultContext): void
{
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
}
public function getSupportedTypes(?string $format): array
{
$isCacheable = __CLASS__ === static::class || $this->hasCacheableSupportsMethod();
return [
\DateTimeInterface::class => $isCacheable,
\DateTimeImmutable::class => $isCacheable,
\DateTime::class => $isCacheable,
];
}
/**
* @throws InvalidArgumentException
*/
public function normalize(mixed $object, ?string $format = null, array $context = []): string
{
if (!$object instanceof \DateTimeInterface) {
throw new InvalidArgumentException('The object must implement the "\DateTimeInterface".');
}
$dateTimeFormat = $context[self::FORMAT_KEY] ?? $this->defaultContext[self::FORMAT_KEY];
$timezone = $this->getTimezone($context);
if (null !== $timezone) {
$object = clone $object;
$object = $object->setTimezone($timezone);
}
return $object->format($dateTimeFormat);
}
/**
* @param array $context
*/
public function supportsNormalization(mixed $data, ?string $format = null /* , array $context = [] */): bool
{
return $data instanceof \DateTimeInterface;
}
/**
* @throws NotNormalizableValueException
*/
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): \DateTimeInterface
{
if (\is_int($data) || \is_float($data)) {
switch ($context[self::FORMAT_KEY] ?? $this->defaultContext[self::FORMAT_KEY] ?? null) {
case 'U': $data = sprintf('%d', $data); break;
case 'U.u': $data = sprintf('%.6F', $data); break;
}
}
if (!\is_string($data) || '' === trim($data)) {
throw NotNormalizableValueException::createForUnexpectedDataType('The data is either not an string, an empty string, or null; you should pass a string that can be parsed with the passed format or a valid DateTime string.', $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true);
}
try {
if (\DateTimeInterface::class === $type) {
$type = \DateTimeImmutable::class;
}
$timezone = $this->getTimezone($context);
$dateTimeFormat = $context[self::FORMAT_KEY] ?? null;
if (null !== $dateTimeFormat) {
if (false !== $object = $type::createFromFormat($dateTimeFormat, $data, $timezone)) {
return $object;
}
$dateTimeErrors = $type::getLastErrors();
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Parsing datetime string "%s" using format "%s" resulted in %d errors: ', $data, $dateTimeFormat, $dateTimeErrors['error_count'])."\n".implode("\n", $this->formatDateTimeErrors($dateTimeErrors['errors'])), $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true);
}
$defaultDateTimeFormat = $this->defaultContext[self::FORMAT_KEY] ?? null;
if (null !== $defaultDateTimeFormat) {
if (false !== $object = $type::createFromFormat($defaultDateTimeFormat, $data, $timezone)) {
return $object;
}
}
return new $type($data, $timezone);
} catch (NotNormalizableValueException $e) {
throw $e;
} catch (\Exception $e) {
throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, false, $e->getCode(), $e);
}
}
/**
* @param array $context
*/
public function supportsDenormalization(mixed $data, string $type, ?string $format = null /* , array $context = [] */): bool
{
return isset(self::SUPPORTED_TYPES[$type]);
}
/**
* @deprecated since Symfony 6.3, use "getSupportedTypes()" instead
*/
public function hasCacheableSupportsMethod(): bool
{
trigger_deprecation('symfony/serializer', '6.3', 'The "%s()" method is deprecated, implement "%s::getSupportedTypes()" instead.', __METHOD__, get_debug_type($this));
return __CLASS__ === static::class;
}
/**
* Formats datetime errors.
*
* @return string[]
*/
private function formatDateTimeErrors(array $errors): array
{
$formattedErrors = [];
foreach ($errors as $pos => $message) {
$formattedErrors[] = sprintf('at position %d: %s', $pos, $message);
}
return $formattedErrors;
}
private function getTimezone(array $context): ?\DateTimeZone
{
$dateTimeZone = $context[self::TIMEZONE_KEY] ?? $this->defaultContext[self::TIMEZONE_KEY];
if (null === $dateTimeZone) {
return null;
}
return $dateTimeZone instanceof \DateTimeZone ? $dateTimeZone : new \DateTimeZone($dateTimeZone);
}
}

View File

@@ -0,0 +1,87 @@
<?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\PropertyInfo\Type;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
/**
* Normalizes a {@see \DateTimeZone} object to a timezone string.
*
* @author Jérôme Desjardins <jewome62@gmail.com>
*
* @final since Symfony 6.3
*/
class DateTimeZoneNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface
{
public function getSupportedTypes(?string $format): array
{
return [
\DateTimeZone::class => __CLASS__ === static::class || $this->hasCacheableSupportsMethod(),
];
}
/**
* @throws InvalidArgumentException
*/
public function normalize(mixed $object, ?string $format = null, array $context = []): string
{
if (!$object instanceof \DateTimeZone) {
throw new InvalidArgumentException('The object must be an instance of "\DateTimeZone".');
}
return $object->getName();
}
/**
* @param array $context
*/
public function supportsNormalization(mixed $data, ?string $format = null /* , array $context = [] */): bool
{
return $data instanceof \DateTimeZone;
}
/**
* @throws NotNormalizableValueException
*/
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): \DateTimeZone
{
if ('' === $data || null === $data) {
throw NotNormalizableValueException::createForUnexpectedDataType('The data is either an empty string or null, you should pass a string that can be parsed as a DateTimeZone.', $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true);
}
try {
return new \DateTimeZone($data);
} catch (\Exception $e) {
throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true, $e->getCode(), $e);
}
}
/**
* @param array $context
*/
public function supportsDenormalization(mixed $data, string $type, ?string $format = null /* , array $context = [] */): bool
{
return \DateTimeZone::class === $type;
}
/**
* @deprecated since Symfony 6.3, use "getSupportedTypes()" instead
*/
public function hasCacheableSupportsMethod(): bool
{
trigger_deprecation('symfony/serializer', '6.3', 'The "%s()" method is deprecated, implement "%s::getSupportedTypes()" instead.', __METHOD__, get_debug_type($this));
return __CLASS__ === static::class;
}
}

View File

@@ -0,0 +1,40 @@
<?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;
/**
* Defines the most basic interface a class must implement to be denormalizable.
*
* If a denormalizer is registered for the class and it doesn't implement
* the Denormalizable interfaces, the normalizer will be used instead
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
interface DenormalizableInterface
{
/**
* Denormalizes the object back from an array of scalars|arrays.
*
* It is important to understand that the denormalize() call should denormalize
* recursively all child objects of the implementor.
*
* @param DenormalizerInterface $denormalizer The denormalizer is given so that you
* can use it to denormalize objects contained within this object
* @param array|string|int|float|bool $data The data from which to re-create the object
* @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 void
*/
public function denormalize(DenormalizerInterface $denormalizer, array|string|int|float|bool $data, ?string $format = null, array $context = []);
}

View File

@@ -0,0 +1,25 @@
<?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;
/**
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
interface DenormalizerAwareInterface
{
/**
* Sets the owning Denormalizer object.
*
* @return void
*/
public function setDenormalizer(DenormalizerInterface $denormalizer);
}

View File

@@ -0,0 +1,31 @@
<?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;
/**
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
trait DenormalizerAwareTrait
{
/**
* @var DenormalizerInterface
*/
protected $denormalizer;
/**
* @return void
*/
public function setDenormalizer(DenormalizerInterface $denormalizer)
{
$this->denormalizer = $denormalizer;
}
}

View File

@@ -0,0 +1,79 @@
<?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\Exception\BadMethodCallException;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\Exception\ExtraAttributesException;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Exception\RuntimeException;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @method array getSupportedTypes(?string $format)
*/
interface DenormalizerInterface
{
public const COLLECT_DENORMALIZATION_ERRORS = 'collect_denormalization_errors';
/**
* Denormalizes data back into an object of the given class.
*
* @param mixed $data Data to restore
* @param string $type The expected class to instantiate
* @param string|null $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
* @throws ExtraAttributesException Occurs when the item doesn't have attribute to receive given data
* @throws LogicException Occurs when the normalizer is not supposed to denormalize
* @throws RuntimeException Occurs if the class cannot be instantiated
* @throws ExceptionInterface Occurs for all the other cases of errors
*/
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []);
/**
* 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|null $format The format being deserialized from
* @param array $context Options available to the denormalizer
*
* @return bool
*/
public function supportsDenormalization(mixed $data, string $type, ?string $format = null /* , array $context = [] */);
/**
* Returns the types potentially supported by this denormalizer.
*
* For each supported formats (if applicable), the supported types should be
* returned as keys, and each type should be mapped to a boolean indicating
* if the result of supportsDenormalization() can be cached or not
* (a result cannot be cached when it depends on the context or on the data.)
* A null value means that the denormalizer does not support the corresponding
* type.
*
* Use type "object" to match any classes or interfaces,
* and type "*" to match any types.
*
* @return array<class-string|'*'|'object'|string, bool|null>
*/
/* public function getSupportedTypes(?string $format): array; */
}

View File

@@ -0,0 +1,95 @@
<?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\Form\FormInterface;
/**
* Normalizes invalid Form instances.
*/
final class FormErrorNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface
{
public const TITLE = 'title';
public const TYPE = 'type';
public const CODE = 'status_code';
public function normalize(mixed $object, ?string $format = null, array $context = []): array
{
$data = [
'title' => $context[self::TITLE] ?? 'Validation Failed',
'type' => $context[self::TYPE] ?? 'https://symfony.com/errors/form',
'code' => $context[self::CODE] ?? null,
'errors' => $this->convertFormErrorsToArray($object),
];
if (0 !== \count($object->all())) {
$data['children'] = $this->convertFormChildrenToArray($object);
}
return $data;
}
public function getSupportedTypes(?string $format): array
{
return [
FormInterface::class => false,
];
}
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
return $data instanceof FormInterface && $data->isSubmitted() && !$data->isValid();
}
private function convertFormErrorsToArray(FormInterface $data): array
{
$errors = [];
foreach ($data->getErrors() as $error) {
$errors[] = [
'message' => $error->getMessage(),
'cause' => $error->getCause(),
];
}
return $errors;
}
private function convertFormChildrenToArray(FormInterface $data): array
{
$children = [];
foreach ($data->all() as $child) {
$childData = [
'errors' => $this->convertFormErrorsToArray($child),
];
if ($child->all()) {
$childData['children'] = $this->convertFormChildrenToArray($child);
}
$children[$child->getName()] = $childData;
}
return $children;
}
/**
* @deprecated since Symfony 6.3, use "getSupportedTypes()" instead
*/
public function hasCacheableSupportsMethod(): bool
{
trigger_deprecation('symfony/serializer', '6.3', 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', __METHOD__);
return true;
}
}

View File

@@ -0,0 +1,225 @@
<?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\Annotation\Ignore as LegacyIgnore;
use Symfony\Component\Serializer\Attribute\Ignore;
/**
* Converts between objects with getter and setter methods and arrays.
*
* The normalization process looks at all public methods and calls the ones
* which have a name starting with get and take no parameters. The result is a
* map from property names (method name stripped of the get prefix and converted
* to lower case) to property values. Property values are normalized through the
* serializer.
*
* The denormalization first looks at the constructor of the given class to see
* if any of the parameters have the same name as one of the properties. The
* constructor is then called with all parameters or an exception is thrown if
* any required parameters were not present as properties. Then the denormalizer
* walks through the given map of property names to property values to see if a
* setter method exists for any of the properties. If a setter exists it is
* called with the property value. No automatic denormalization of the value
* takes place.
*
* @author Nils Adermann <naderman@naderman.de>
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @final since Symfony 6.3
*/
class GetSetMethodNormalizer extends AbstractObjectNormalizer
{
private static $reflectionCache = [];
private static array $setterAccessibleCache = [];
public function getSupportedTypes(?string $format): array
{
return ['object' => __CLASS__ === static::class || $this->hasCacheableSupportsMethod()];
}
/**
* @param array $context
*/
public function supportsNormalization(mixed $data, ?string $format = null /* , array $context = [] */): bool
{
return parent::supportsNormalization($data, $format) && $this->supports($data::class, true);
}
/**
* @param array $context
*/
public function supportsDenormalization(mixed $data, string $type, ?string $format = null /* , array $context = [] */): bool
{
return parent::supportsDenormalization($data, $type, $format) && $this->supports($type, false);
}
/**
* @deprecated since Symfony 6.3, use "getSupportedTypes()" instead
*/
public function hasCacheableSupportsMethod(): bool
{
trigger_deprecation('symfony/serializer', '6.3', 'The "%s()" method is deprecated, implement "%s::getSupportedTypes()" instead.', __METHOD__, get_debug_type($this));
return __CLASS__ === static::class;
}
/**
* Checks if the given class has any getter or setter method.
*/
private function supports(string $class, bool $readAttributes): bool
{
if ($this->classDiscriminatorResolver?->getMappingForClass($class)) {
return true;
}
if (!isset(self::$reflectionCache[$class])) {
self::$reflectionCache[$class] = new \ReflectionClass($class);
}
$reflection = self::$reflectionCache[$class];
foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflectionMethod) {
if ($readAttributes ? $this->isGetMethod($reflectionMethod) : $this->isSetMethod($reflectionMethod)) {
return true;
}
}
return false;
}
/**
* Checks if a method's name matches /^(get|is|has).+$/ and can be called non-statically without parameters.
*/
private function isGetMethod(\ReflectionMethod $method): bool
{
return !$method->isStatic()
&& !($method->getAttributes(Ignore::class) || $method->getAttributes(LegacyIgnore::class))
&& !$method->getNumberOfRequiredParameters()
&& ((2 < ($methodLength = \strlen($method->name)) && str_starts_with($method->name, 'is'))
|| (3 < $methodLength && (str_starts_with($method->name, 'has') || str_starts_with($method->name, 'get')))
);
}
/**
* Checks if a method's name matches /^set.+$/ and can be called non-statically with one parameter.
*/
private function isSetMethod(\ReflectionMethod $method): bool
{
return !$method->isStatic()
&& !$method->getAttributes(Ignore::class)
&& 0 < $method->getNumberOfParameters()
&& str_starts_with($method->name, 'set');
}
protected function extractAttributes(object $object, ?string $format = null, array $context = []): array
{
$reflectionObject = new \ReflectionObject($object);
$reflectionMethods = $reflectionObject->getMethods(\ReflectionMethod::IS_PUBLIC);
$attributes = [];
foreach ($reflectionMethods as $method) {
if (!$this->isGetMethod($method)) {
continue;
}
$attributeName = lcfirst(substr($method->name, str_starts_with($method->name, 'is') ? 2 : 3));
if ($this->isAllowedAttribute($object, $attributeName, $format, $context)) {
$attributes[] = $attributeName;
}
}
return $attributes;
}
protected function getAttributeValue(object $object, string $attribute, ?string $format = null, array $context = []): mixed
{
$getter = 'get'.$attribute;
if (method_exists($object, $getter) && \is_callable([$object, $getter])) {
return $object->$getter();
}
$isser = 'is'.$attribute;
if (method_exists($object, $isser) && \is_callable([$object, $isser])) {
return $object->$isser();
}
$haser = 'has'.$attribute;
if (method_exists($object, $haser) && \is_callable([$object, $haser])) {
return $object->$haser();
}
return null;
}
/**
* @return void
*/
protected function setAttributeValue(object $object, string $attribute, mixed $value, ?string $format = null, array $context = [])
{
$setter = 'set'.$attribute;
$key = $object::class.':'.$setter;
if (!isset(self::$setterAccessibleCache[$key])) {
self::$setterAccessibleCache[$key] = method_exists($object, $setter) && \is_callable([$object, $setter]) && !(new \ReflectionMethod($object, $setter))->isStatic();
}
if (self::$setterAccessibleCache[$key]) {
$object->$setter($value);
}
}
protected function isAllowedAttribute($classOrObject, string $attribute, ?string $format = null, array $context = [])
{
if (!parent::isAllowedAttribute($classOrObject, $attribute, $format, $context)) {
return false;
}
$class = \is_object($classOrObject) ? \get_class($classOrObject) : $classOrObject;
if (!isset(self::$reflectionCache[$class])) {
self::$reflectionCache[$class] = new \ReflectionClass($class);
}
$reflection = self::$reflectionCache[$class];
if ($context['_read_attributes'] ?? true) {
foreach (['get', 'is', 'has'] as $getterPrefix) {
$getter = $getterPrefix.$attribute;
$reflectionMethod = $reflection->hasMethod($getter) ? $reflection->getMethod($getter) : null;
if ($reflectionMethod && $this->isGetMethod($reflectionMethod)) {
return true;
}
}
return false;
}
$setter = 'set'.$attribute;
if ($reflection->hasMethod($setter) && $this->isSetMethod($reflection->getMethod($setter))) {
return true;
}
$constructor = $reflection->getConstructor();
if ($constructor && $constructor->isPublic()) {
foreach ($constructor->getParameters() as $parameter) {
if ($parameter->getName() === $attribute) {
return true;
}
}
}
return false;
}
}

View File

@@ -0,0 +1,80 @@
<?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\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\LogicException;
/**
* A normalizer that uses an objects own JsonSerializable implementation.
*
* @author Fred Cox <mcfedr@gmail.com>
*
* @final since Symfony 6.3
*/
class JsonSerializableNormalizer extends AbstractNormalizer
{
public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
{
if ($this->isCircularReference($object, $context)) {
return $this->handleCircularReference($object, $format, $context);
}
if (!$object instanceof \JsonSerializable) {
throw new InvalidArgumentException(sprintf('The object must implement "%s".', \JsonSerializable::class));
}
if (!$this->serializer instanceof NormalizerInterface) {
throw new LogicException('Cannot normalize object because injected serializer is not a normalizer.');
}
return $this->serializer->normalize($object->jsonSerialize(), $format, $context);
}
public function getSupportedTypes(?string $format): array
{
return [
\JsonSerializable::class => __CLASS__ === static::class || $this->hasCacheableSupportsMethod(),
];
}
/**
* @param array $context
*/
public function supportsNormalization(mixed $data, ?string $format = null /* , array $context = [] */): bool
{
return $data instanceof \JsonSerializable;
}
/**
* @param array $context
*/
public function supportsDenormalization(mixed $data, string $type, ?string $format = null /* , array $context = [] */): bool
{
return false;
}
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed
{
throw new LogicException(sprintf('Cannot denormalize with "%s".', \JsonSerializable::class));
}
/**
* @deprecated since Symfony 6.3, use "getSupportedTypes()" instead
*/
public function hasCacheableSupportsMethod(): bool
{
trigger_deprecation('symfony/serializer', '6.3', 'The "%s()" method is deprecated, implement "%s::getSupportedTypes()" instead.', __METHOD__, get_debug_type($this));
return __CLASS__ === static::class;
}
}

View File

@@ -0,0 +1,133 @@
<?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\Mime\Address;
use Symfony\Component\Mime\Header\HeaderInterface;
use Symfony\Component\Mime\Header\Headers;
use Symfony\Component\Mime\Header\UnstructuredHeader;
use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\Part\AbstractPart;
use Symfony\Component\Mime\RawMessage;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerInterface;
/**
* Normalize Mime message classes.
*
* It forces the use of a PropertyNormalizer instance for normalization
* of all data objects composing a Message.
*
* Emails using resources for any parts are not serializable.
*/
final class MimeMessageNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface, CacheableSupportsMethodInterface
{
private NormalizerInterface&DenormalizerInterface $serializer;
private array $headerClassMap;
private \ReflectionProperty $headersProperty;
public function __construct(private readonly PropertyNormalizer $normalizer)
{
$this->headerClassMap = (new \ReflectionClassConstant(Headers::class, 'HEADER_CLASS_MAP'))->getValue();
$this->headersProperty = new \ReflectionProperty(Headers::class, 'headers');
}
public function getSupportedTypes(?string $format): array
{
$isCacheable = __CLASS__ === static::class || $this->hasCacheableSupportsMethod();
return [
Message::class => $isCacheable,
Headers::class => $isCacheable,
HeaderInterface::class => $isCacheable,
Address::class => $isCacheable,
AbstractPart::class => $isCacheable,
];
}
public function setSerializer(SerializerInterface $serializer): void
{
if (!$serializer instanceof NormalizerInterface || !$serializer instanceof DenormalizerInterface) {
throw new LogicException(sprintf('The passed serializer should implement both NormalizerInterface and DenormalizerInterface, "%s" given.', get_debug_type($serializer)));
}
$this->serializer = $serializer;
$this->normalizer->setSerializer($serializer);
}
public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
{
if ($object instanceof Headers) {
$ret = [];
foreach ($this->headersProperty->getValue($object) as $name => $header) {
$ret[$name] = $this->serializer->normalize($header, $format, $context);
}
return $ret;
}
$ret = $this->normalizer->normalize($object, $format, $context);
if ($object instanceof AbstractPart) {
$ret['class'] = $object::class;
unset($ret['seekable'], $ret['cid'], $ret['handle']);
}
if ($object instanceof RawMessage && \array_key_exists('message', $ret) && null === $ret['message']) {
unset($ret['message']);
}
return $ret;
}
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed
{
if (Headers::class === $type) {
$ret = [];
foreach ($data as $headers) {
foreach ($headers as $header) {
$ret[] = $this->serializer->denormalize($header, $this->headerClassMap[strtolower($header['name'])] ?? UnstructuredHeader::class, $format, $context);
}
}
return new Headers(...$ret);
}
if (AbstractPart::class === $type) {
$type = $data['class'];
unset($data['class']);
$data['headers'] = $this->serializer->denormalize($data['headers'], Headers::class, $format, $context);
}
return $this->normalizer->denormalize($data, $type, $format, $context);
}
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
return $data instanceof Message || $data instanceof Headers || $data instanceof HeaderInterface || $data instanceof Address || $data instanceof AbstractPart;
}
public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
{
return is_a($type, Message::class, true) || Headers::class === $type || AbstractPart::class === $type;
}
/**
* @deprecated since Symfony 6.3, use "getSupportedTypes()" instead
*/
public function hasCacheableSupportsMethod(): bool
{
trigger_deprecation('symfony/serializer', '6.3', 'The "%s()" method is deprecated, implement "%s::getSupportedTypes()" instead.', __METHOD__, get_debug_type($this));
return true;
}
}

View File

@@ -0,0 +1,37 @@
<?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;
/**
* Defines the most basic interface a class must implement to be normalizable.
*
* If a normalizer is registered for the class and it doesn't implement
* the Normalizable interfaces, the normalizer will be used instead.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
interface NormalizableInterface
{
/**
* Normalizes the object into an array of scalars|arrays.
*
* It is important to understand that the normalize() call should normalize
* recursively all child objects of the implementor.
*
* @param NormalizerInterface $normalizer The normalizer is given so that you
* can use it to normalize objects contained within this object
* @param string|null $format The format is optionally given to be able to normalize differently
* based on different output formats
* @param array $context Options for normalizing this object
*/
public function normalize(NormalizerInterface $normalizer, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null;
}

View File

@@ -0,0 +1,25 @@
<?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;
/**
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
interface NormalizerAwareInterface
{
/**
* Sets the owning Normalizer object.
*
* @return void
*/
public function setNormalizer(NormalizerInterface $normalizer);
}

View File

@@ -0,0 +1,31 @@
<?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;
/**
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
trait NormalizerAwareTrait
{
/**
* @var NormalizerInterface
*/
protected $normalizer;
/**
* @return void
*/
public function setNormalizer(NormalizerInterface $normalizer)
{
$this->normalizer = $normalizer;
}
}

View File

@@ -0,0 +1,70 @@
<?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\Exception\CircularReferenceException;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\LogicException;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @method array getSupportedTypes(?string $format)
*/
interface NormalizerInterface
{
/**
* Normalizes an object into a set of arrays/scalars.
*
* @param mixed $object Object to normalize
* @param string|null $format Format the normalization result will be encoded as
* @param array $context Context options for the normalizer
*
* @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
* reference handler can fix it
* @throws LogicException Occurs when the normalizer is not called in an expected context
* @throws ExceptionInterface Occurs for all the other cases of errors
*/
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|null $format The format being (de-)serialized from or into
* @param array $context Context options for the normalizer
*
* @return bool
*/
public function supportsNormalization(mixed $data, ?string $format = null /* , array $context = [] */);
/**
* Returns the types potentially supported by this normalizer.
*
* For each supported formats (if applicable), the supported types should be
* returned as keys, and each type should be mapped to a boolean indicating
* if the result of supportsNormalization() can be cached or not
* (a result cannot be cached when it depends on the context or on the data.)
* A null value means that the normalizer does not support the corresponding
* type.
*
* Use type "object" to match any classes or interfaces,
* and type "*" to match any types.
*
* @return array<class-string|'*'|'object'|string, bool|null>
*/
/* public function getSupportedTypes(?string $format): array; */
}

View File

@@ -0,0 +1,231 @@
<?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\PropertyAccess\Exception\NoSuchPropertyException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyWriteInfo;
use Symfony\Component\Serializer\Annotation\Ignore;
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;
/**
* Converts between objects and arrays using the PropertyAccess component.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @final since Symfony 6.3
*/
class ObjectNormalizer extends AbstractObjectNormalizer
{
private static $reflectionCache = [];
private static $isReadableCache = [];
private static $isWritableCache = [];
protected $propertyAccessor;
protected $propertyInfoExtractor;
private $writeInfoExtractor;
private readonly \Closure $objectClassResolver;
public function __construct(?ClassMetadataFactoryInterface $classMetadataFactory = null, ?NameConverterInterface $nameConverter = null, ?PropertyAccessorInterface $propertyAccessor = null, ?PropertyTypeExtractorInterface $propertyTypeExtractor = null, ?ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, ?callable $objectClassResolver = null, array $defaultContext = [], ?PropertyInfoExtractorInterface $propertyInfoExtractor = null)
{
if (!class_exists(PropertyAccess::class)) {
throw new LogicException('The ObjectNormalizer class requires the "PropertyAccess" component. Try running "composer require symfony/property-access".');
}
parent::__construct($classMetadataFactory, $nameConverter, $propertyTypeExtractor, $classDiscriminatorResolver, $objectClassResolver, $defaultContext);
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
$this->objectClassResolver = ($objectClassResolver ?? static fn ($class) => \is_object($class) ? $class::class : $class)(...);
$this->propertyInfoExtractor = $propertyInfoExtractor ?: new ReflectionExtractor();
$this->writeInfoExtractor = new ReflectionExtractor();
}
public function getSupportedTypes(?string $format): array
{
return ['object' => __CLASS__ === static::class || $this->hasCacheableSupportsMethod()];
}
/**
* @deprecated since Symfony 6.3, use "getSupportedTypes()" instead
*/
public function hasCacheableSupportsMethod(): bool
{
trigger_deprecation('symfony/serializer', '6.3', 'The "%s()" method is deprecated, implement "%s::getSupportedTypes()" instead.', __METHOD__, get_debug_type($this));
return __CLASS__ === static::class;
}
protected function extractAttributes(object $object, ?string $format = null, array $context = []): array
{
if (\stdClass::class === $object::class) {
return array_keys((array) $object);
}
// If not using groups, detect manually
$attributes = [];
// methods
$class = ($this->objectClassResolver)($object);
$reflClass = new \ReflectionClass($class);
foreach ($reflClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflMethod) {
if (
0 !== $reflMethod->getNumberOfRequiredParameters()
|| $reflMethod->isStatic()
|| $reflMethod->isConstructor()
|| $reflMethod->isDestructor()
) {
continue;
}
$name = $reflMethod->name;
$attributeName = null;
if (str_starts_with($name, 'get') || str_starts_with($name, 'has') || str_starts_with($name, 'can')) {
// getters, hassers and canners
$attributeName = substr($name, 3);
if (!$reflClass->hasProperty($attributeName)) {
$attributeName = lcfirst($attributeName);
}
} elseif (str_starts_with($name, 'is')) {
// issers
$attributeName = substr($name, 2);
if (!$reflClass->hasProperty($attributeName)) {
$attributeName = lcfirst($attributeName);
}
}
if (null !== $attributeName && $this->isAllowedAttribute($object, $attributeName, $format, $context)) {
$attributes[$attributeName] = true;
}
}
// properties
foreach ($reflClass->getProperties() as $reflProperty) {
if (!$reflProperty->isPublic()) {
continue;
}
if ($reflProperty->isStatic() || !$this->isAllowedAttribute($object, $reflProperty->name, $format, $context)) {
continue;
}
$attributes[$reflProperty->name] = true;
}
return array_keys($attributes);
}
protected function getAttributeValue(object $object, string $attribute, ?string $format = null, array $context = []): mixed
{
$mapping = $this->classDiscriminatorResolver?->getMappingForMappedObject($object);
return $attribute === $mapping?->getTypeProperty()
? $mapping
: $this->propertyAccessor->getValue($object, $attribute);
}
/**
* @return void
*/
protected function setAttributeValue(object $object, string $attribute, mixed $value, ?string $format = null, array $context = [])
{
try {
$this->propertyAccessor->setValue($object, $attribute, $value);
} catch (NoSuchPropertyException) {
// Properties not found are ignored
}
}
protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false): array|bool
{
if (false === $allowedAttributes = parent::getAllowedAttributes($classOrObject, $context, $attributesAsString)) {
return false;
}
if (null !== $this->classDiscriminatorResolver) {
$class = \is_object($classOrObject) ? $classOrObject::class : $classOrObject;
if (null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForMappedObject($classOrObject)) {
$allowedAttributes[] = $attributesAsString ? $discriminatorMapping->getTypeProperty() : new AttributeMetadata($discriminatorMapping->getTypeProperty());
}
if (null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForClass($class)) {
$attributes = [];
foreach ($discriminatorMapping->getTypesMapping() as $mappedClass) {
$attributes[] = parent::getAllowedAttributes($mappedClass, $context, $attributesAsString);
}
$allowedAttributes = array_merge($allowedAttributes, ...$attributes);
}
}
return $allowedAttributes;
}
protected function isAllowedAttribute($classOrObject, string $attribute, ?string $format = null, array $context = [])
{
if (!parent::isAllowedAttribute($classOrObject, $attribute, $format, $context)) {
return false;
}
$class = \is_object($classOrObject) ? \get_class($classOrObject) : $classOrObject;
if ($context['_read_attributes'] ?? true) {
if (!isset(self::$isReadableCache[$class.$attribute])) {
self::$isReadableCache[$class.$attribute] = (\is_object($classOrObject) && $this->propertyAccessor->isReadable($classOrObject, $attribute)) || $this->propertyInfoExtractor->isReadable($class, $attribute) || $this->hasAttributeAccessorMethod($class, $attribute);
}
return self::$isReadableCache[$class.$attribute];
}
if (!isset(self::$isWritableCache[$class.$attribute])) {
if (str_contains($attribute, '.')) {
self::$isWritableCache[$class.$attribute] = true;
} else {
self::$isWritableCache[$class.$attribute] = $this->propertyInfoExtractor->isWritable($class, $attribute)
|| (($writeInfo = $this->writeInfoExtractor->getWriteInfo($class, $attribute)) && PropertyWriteInfo::TYPE_NONE !== $writeInfo->getType());
}
}
return self::$isWritableCache[$class.$attribute];
}
private function hasAttributeAccessorMethod(string $class, string $attribute): bool
{
if (!isset(self::$reflectionCache[$class])) {
self::$reflectionCache[$class] = new \ReflectionClass($class);
}
$reflection = self::$reflectionCache[$class];
if (!$reflection->hasMethod($attribute)) {
return false;
}
$method = $reflection->getMethod($attribute);
return !$method->isStatic()
&& !$method->getAttributes(Ignore::class)
&& !$method->getNumberOfRequiredParameters();
}
}

View File

@@ -0,0 +1,34 @@
<?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;
trait ObjectToPopulateTrait
{
/**
* Extract the `object_to_populate` field from the context if it exists
* and is an instance of the provided $class.
*
* @param string $class The class the object should be
* @param string|null $key They in which to look for the object to populate.
* Keeps backwards compatibility with `AbstractNormalizer`.
*/
protected function extractObjectToPopulate(string $class, array $context, ?string $key = null): ?object
{
$key ??= AbstractNormalizer::OBJECT_TO_POPULATE;
if (isset($context[$key]) && \is_object($context[$key]) && $context[$key] instanceof $class) {
return $context[$key];
}
return null;
}
}

View File

@@ -0,0 +1,127 @@
<?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\ErrorHandler\Exception\FlattenException;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\Messenger\Exception\ValidationFailedException as MessageValidationFailedException;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\PartialDenormalizationException;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerAwareTrait;
use Symfony\Component\Validator\Exception\ValidationFailedException;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Normalizes errors according to the API Problem spec (RFC 7807).
*
* @see https://tools.ietf.org/html/rfc7807
*
* @author Kévin Dunglas <dunglas@gmail.com>
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class ProblemNormalizer implements NormalizerInterface, SerializerAwareInterface, CacheableSupportsMethodInterface
{
use SerializerAwareTrait;
public const TITLE = 'title';
public const TYPE = 'type';
public const STATUS = 'status';
public function __construct(
private bool $debug = false,
private array $defaultContext = [],
private ?TranslatorInterface $translator = null,
) {
}
public function getSupportedTypes(?string $format): array
{
return [
FlattenException::class => __CLASS__ === self::class || $this->hasCacheableSupportsMethod(),
];
}
public function normalize(mixed $object, ?string $format = null, array $context = []): array
{
if (!$object instanceof FlattenException) {
throw new InvalidArgumentException(sprintf('The object must implement "%s".', FlattenException::class));
}
$data = [];
$context += $this->defaultContext;
$debug = $this->debug && ($context['debug'] ?? true);
$exception = $context['exception'] ?? null;
if ($exception instanceof HttpExceptionInterface) {
$exception = $exception->getPrevious();
if ($exception instanceof PartialDenormalizationException) {
$trans = $this->translator ? $this->translator->trans(...) : fn ($m, $p) => strtr($m, $p);
$template = 'This value should be of type {{ type }}.';
$data = [
self::TYPE => 'https://symfony.com/errors/validation',
self::TITLE => 'Validation Failed',
'violations' => array_map(
fn ($e) => [
'propertyPath' => $e->getPath(),
'title' => $trans($template, [
'{{ type }}' => implode('|', $e->getExpectedTypes() ?? ['?']),
], 'validators'),
'template' => $template,
'parameters' => [
'{{ type }}' => implode('|', $e->getExpectedTypes() ?? ['?']),
],
] + ($debug || $e->canUseMessageForUser() ? ['hint' => $e->getMessage()] : []),
$exception->getErrors()
),
];
$data['detail'] = implode("\n", array_map(fn ($e) => $e['propertyPath'].': '.$e['title'], $data['violations']));
} elseif (($exception instanceof ValidationFailedException || $exception instanceof MessageValidationFailedException)
&& $this->serializer instanceof NormalizerInterface
&& $this->serializer->supportsNormalization($exception->getViolations(), $format, $context)
) {
$data = $this->serializer->normalize($exception->getViolations(), $format, $context);
}
}
$data = [
self::TYPE => $data[self::TYPE] ?? $context[self::TYPE] ?? 'https://tools.ietf.org/html/rfc2616#section-10',
self::TITLE => $data[self::TITLE] ?? $context[self::TITLE] ?? 'An error occurred',
self::STATUS => $context[self::STATUS] ?? $object->getStatusCode(),
'detail' => $data['detail'] ?? ($debug ? $object->getMessage() : $object->getStatusText()),
] + $data;
if ($debug) {
$data['class'] = $object->getClass();
$data['trace'] = $object->getTrace();
}
return $data;
}
/**
* @param array $context
*/
public function supportsNormalization(mixed $data, ?string $format = null /* , array $context = [] */): bool
{
return $data instanceof FlattenException;
}
/**
* @deprecated since Symfony 6.3, use "getSupportedTypes()" instead
*/
public function hasCacheableSupportsMethod(): bool
{
trigger_deprecation('symfony/serializer', '6.3', 'The "%s()" method is deprecated, implement "%s::getSupportedTypes()" instead.', __METHOD__, get_debug_type($this));
return true;
}
}

View File

@@ -0,0 +1,224 @@
<?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\PropertyAccess\Exception\UninitializedPropertyException;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
/**
* Converts between objects and arrays by mapping properties.
*
* The normalization process looks for all the object's properties (public and private).
* The result is a map from property names to property values. Property values
* are normalized through the serializer.
*
* The denormalization first looks at the constructor of the given class to see
* if any of the parameters have the same name as one of the properties. The
* constructor is then called with all parameters or an exception is thrown if
* any required parameters were not present as properties. Then the denormalizer
* walks through the given map of property names to property values to see if a
* property with the corresponding name exists. If found, the property gets the value.
*
* @author Matthieu Napoli <matthieu@mnapoli.fr>
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @final since Symfony 6.3
*/
class PropertyNormalizer extends AbstractObjectNormalizer
{
public const NORMALIZE_PUBLIC = 1;
public const NORMALIZE_PROTECTED = 2;
public const NORMALIZE_PRIVATE = 4;
/**
* Flag to control whether fields should be output based on visibility.
*/
public const NORMALIZE_VISIBILITY = 'normalize_visibility';
public function __construct(?ClassMetadataFactoryInterface $classMetadataFactory = null, ?NameConverterInterface $nameConverter = null, ?PropertyTypeExtractorInterface $propertyTypeExtractor = null, ?ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, ?callable $objectClassResolver = null, array $defaultContext = [])
{
parent::__construct($classMetadataFactory, $nameConverter, $propertyTypeExtractor, $classDiscriminatorResolver, $objectClassResolver, $defaultContext);
if (!isset($this->defaultContext[self::NORMALIZE_VISIBILITY])) {
$this->defaultContext[self::NORMALIZE_VISIBILITY] = self::NORMALIZE_PUBLIC | self::NORMALIZE_PROTECTED | self::NORMALIZE_PRIVATE;
}
}
public function getSupportedTypes(?string $format): array
{
return ['object' => __CLASS__ === static::class || $this->hasCacheableSupportsMethod()];
}
/**
* @param array $context
*/
public function supportsNormalization(mixed $data, ?string $format = null /* , array $context = [] */): bool
{
return parent::supportsNormalization($data, $format) && $this->supports($data::class);
}
/**
* @param array $context
*/
public function supportsDenormalization(mixed $data, string $type, ?string $format = null /* , array $context = [] */): bool
{
return parent::supportsDenormalization($data, $type, $format) && $this->supports($type);
}
/**
* @deprecated since Symfony 6.3, use "getSupportedTypes()" instead
*/
public function hasCacheableSupportsMethod(): bool
{
trigger_deprecation('symfony/serializer', '6.3', 'The "%s()" method is deprecated, implement "%s::getSupportedTypes()" instead.', __METHOD__, get_debug_type($this));
return __CLASS__ === static::class;
}
/**
* Checks if the given class has any non-static property.
*/
private function supports(string $class): bool
{
if ($this->classDiscriminatorResolver?->getMappingForClass($class)) {
return true;
}
$class = new \ReflectionClass($class);
// We look for at least one non-static property
do {
foreach ($class->getProperties() as $property) {
if (!$property->isStatic()) {
return true;
}
}
} while ($class = $class->getParentClass());
return false;
}
protected function isAllowedAttribute(object|string $classOrObject, string $attribute, ?string $format = null, array $context = []): bool
{
if (!parent::isAllowedAttribute($classOrObject, $attribute, $format, $context)) {
return false;
}
try {
$reflectionProperty = $this->getReflectionProperty($classOrObject, $attribute);
} catch (\ReflectionException) {
return false;
}
if ($reflectionProperty->isStatic()) {
return false;
}
$normalizeVisibility = $context[self::NORMALIZE_VISIBILITY] ?? $this->defaultContext[self::NORMALIZE_VISIBILITY];
if ((self::NORMALIZE_PUBLIC & $normalizeVisibility) && $reflectionProperty->isPublic()) {
return true;
}
if ((self::NORMALIZE_PROTECTED & $normalizeVisibility) && $reflectionProperty->isProtected()) {
return true;
}
if ((self::NORMALIZE_PRIVATE & $normalizeVisibility) && $reflectionProperty->isPrivate()) {
return true;
}
return false;
}
protected function extractAttributes(object $object, ?string $format = null, array $context = []): array
{
$reflectionObject = new \ReflectionObject($object);
$attributes = [];
do {
foreach ($reflectionObject->getProperties() as $property) {
if (!$this->isAllowedAttribute($reflectionObject->getName(), $property->name, $format, $context)) {
continue;
}
$attributes[] = $property->name;
}
} while ($reflectionObject = $reflectionObject->getParentClass());
return array_unique($attributes);
}
protected function getAttributeValue(object $object, string $attribute, ?string $format = null, array $context = []): mixed
{
try {
$reflectionProperty = $this->getReflectionProperty($object, $attribute);
} catch (\ReflectionException) {
return null;
}
if ($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 UninitializedPropertyException(sprintf('The property "%s::$%s" is not initialized.', $object::class, $reflectionProperty->name));
}
}
return $reflectionProperty->getValue($object);
}
/**
* @return void
*/
protected function setAttributeValue(object $object, string $attribute, mixed $value, ?string $format = null, array $context = [])
{
try {
$reflectionProperty = $this->getReflectionProperty($object, $attribute);
} catch (\ReflectionException) {
return;
}
if ($reflectionProperty->isStatic()) {
return;
}
$reflectionProperty->setValue($object, $value);
}
/**
* @throws \ReflectionException
*/
private function getReflectionProperty(string|object $classOrObject, string $attribute): \ReflectionProperty
{
$reflectionClass = new \ReflectionClass($classOrObject);
while (true) {
try {
return $reflectionClass->getProperty($attribute);
} catch (\ReflectionException $e) {
if (!$reflectionClass = $reflectionClass->getParentClass()) {
throw $e;
}
}
}
}
}

View File

@@ -0,0 +1,55 @@
<?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\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
use Symfony\Contracts\Translation\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
final class TranslatableNormalizer implements NormalizerInterface
{
public const NORMALIZATION_LOCALE_KEY = 'translatable_normalization_locale';
private array $defaultContext = [
self::NORMALIZATION_LOCALE_KEY => null,
];
public function __construct(
private readonly TranslatorInterface $translator,
array $defaultContext = [],
) {
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
}
/**
* @throws InvalidArgumentException
*/
public function normalize(mixed $object, ?string $format = null, array $context = []): string
{
if (!$object instanceof TranslatableInterface) {
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The object must implement the "%s".', TranslatableInterface::class), $object, [TranslatableInterface::class]);
}
return $object->trans($this->translator, $context[self::NORMALIZATION_LOCALE_KEY] ?? $this->defaultContext[self::NORMALIZATION_LOCALE_KEY]);
}
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
return $data instanceof TranslatableInterface;
}
public function getSupportedTypes(?string $format): array
{
return [TranslatableInterface::class => true];
}
}

View File

@@ -0,0 +1,111 @@
<?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\PropertyInfo\Type;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
use Symfony\Component\Uid\AbstractUid;
use Symfony\Component\Uid\Uuid;
final class UidNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface
{
public const NORMALIZATION_FORMAT_KEY = 'uid_normalization_format';
public const NORMALIZATION_FORMAT_CANONICAL = 'canonical';
public const NORMALIZATION_FORMAT_BASE58 = 'base58';
public const NORMALIZATION_FORMAT_BASE32 = 'base32';
public const NORMALIZATION_FORMAT_RFC4122 = 'rfc4122';
public const NORMALIZATION_FORMATS = [
self::NORMALIZATION_FORMAT_CANONICAL,
self::NORMALIZATION_FORMAT_BASE58,
self::NORMALIZATION_FORMAT_BASE32,
self::NORMALIZATION_FORMAT_RFC4122,
];
private array $defaultContext = [
self::NORMALIZATION_FORMAT_KEY => self::NORMALIZATION_FORMAT_CANONICAL,
];
public function __construct(array $defaultContext = [])
{
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
}
public function getSupportedTypes(?string $format): array
{
return [
AbstractUid::class => true,
];
}
/**
* @param AbstractUid $object
*/
public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
{
return match ($context[self::NORMALIZATION_FORMAT_KEY] ?? $this->defaultContext[self::NORMALIZATION_FORMAT_KEY]) {
self::NORMALIZATION_FORMAT_CANONICAL => (string) $object,
self::NORMALIZATION_FORMAT_BASE58 => $object->toBase58(),
self::NORMALIZATION_FORMAT_BASE32 => $object->toBase32(),
self::NORMALIZATION_FORMAT_RFC4122 => $object->toRfc4122(),
default => throw new LogicException(sprintf('The "%s" format is not valid.', $context[self::NORMALIZATION_FORMAT_KEY] ?? $this->defaultContext[self::NORMALIZATION_FORMAT_KEY])),
};
}
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
return $data instanceof AbstractUid;
}
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed
{
try {
if (AbstractUid::class === $type) {
trigger_deprecation('symfony/serializer', '6.1', 'Denormalizing to an abstract class in "%s" is deprecated.', __CLASS__);
return Uuid::fromString($data);
}
return $type::fromString($data);
} catch (\InvalidArgumentException|\TypeError) {
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The data is not a valid "%s" string representation.', $type), $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true);
} catch (\Error $e) { // @deprecated remove this catch block in 7.0
if (str_starts_with($e->getMessage(), 'Cannot instantiate abstract class')) {
return $this->denormalize($data, AbstractUid::class, $format, $context);
}
throw $e;
}
}
public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
{
if (AbstractUid::class === $type) {
trigger_deprecation('symfony/serializer', '6.1', 'Supporting denormalization for the "%s" type in "%s" is deprecated, use one of "%s" child class instead.', AbstractUid::class, __CLASS__, AbstractUid::class);
return true;
}
return is_subclass_of($type, AbstractUid::class, true);
}
/**
* @deprecated since Symfony 6.3, use "getSupportedTypes()" instead
*/
public function hasCacheableSupportsMethod(): bool
{
trigger_deprecation('symfony/serializer', '6.3', 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', __METHOD__);
return true;
}
}

View File

@@ -0,0 +1,75 @@
<?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\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerAwareTrait;
/**
* @author Eduard Bulava <bulavaeduard@gmail.com>
*/
final class UnwrappingDenormalizer implements DenormalizerInterface, SerializerAwareInterface, CacheableSupportsMethodInterface
{
use SerializerAwareTrait;
public const UNWRAP_PATH = 'unwrap_path';
private readonly PropertyAccessorInterface $propertyAccessor;
public function __construct(?PropertyAccessorInterface $propertyAccessor = null)
{
$this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
}
public function getSupportedTypes(?string $format): array
{
return ['*' => false];
}
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed
{
$propertyPath = $context[self::UNWRAP_PATH];
$context['unwrapped'] = true;
if ($propertyPath) {
if (!$this->propertyAccessor->isReadable($data, $propertyPath)) {
return null;
}
$data = $this->propertyAccessor->getValue($data, $propertyPath);
}
if (!$this->serializer instanceof DenormalizerInterface) {
throw new LogicException('Cannot unwrap path because the injected serializer is not a denormalizer.');
}
return $this->serializer->denormalize($data, $type, $format, $context);
}
public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
{
return \array_key_exists(self::UNWRAP_PATH, $context) && !isset($context['unwrapped']);
}
/**
* @deprecated since Symfony 6.3, use "getSupportedTypes()" instead
*/
public function hasCacheableSupportsMethod(): bool
{
trigger_deprecation('symfony/serializer', '6.3', 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', __METHOD__);
return $this->serializer instanceof CacheableSupportsMethodInterface && $this->serializer->hasCacheableSupportsMethod();
}
}