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,23 @@
<?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\Annotation;
// do not deprecate in 6.4/7.0, to make it easier for the ecosystem to support 6.4, 7.4 and 8.0 simultaneously
class_exists(\Symfony\Component\Serializer\Attribute\Context::class);
if (false) {
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class Context extends \Symfony\Component\Serializer\Attribute\Context
{
}
}

View File

@@ -0,0 +1,21 @@
<?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\Annotation;
class_exists(\Symfony\Component\Serializer\Attribute\DiscriminatorMap::class);
if (false) {
#[\Attribute(\Attribute::TARGET_CLASS)]
class DiscriminatorMap extends \Symfony\Component\Serializer\Attribute\DiscriminatorMap
{
}
}

View File

@@ -0,0 +1,21 @@
<?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\Annotation;
class_exists(\Symfony\Component\Serializer\Attribute\Groups::class);
if (false) {
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_CLASS)]
class Groups extends \Symfony\Component\Serializer\Attribute\Groups
{
}
}

View File

@@ -0,0 +1,21 @@
<?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\Annotation;
class_exists(\Symfony\Component\Serializer\Attribute\Ignore::class);
if (false) {
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
class Ignore extends \Symfony\Component\Serializer\Attribute\Ignore
{
}
}

View File

@@ -0,0 +1,21 @@
<?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\Annotation;
class_exists(\Symfony\Component\Serializer\Attribute\MaxDepth::class);
if (false) {
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
class MaxDepth extends \Symfony\Component\Serializer\Attribute\MaxDepth
{
}
}

View File

@@ -0,0 +1,21 @@
<?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\Annotation;
class_exists(\Symfony\Component\Serializer\Attribute\SerializedName::class);
if (false) {
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
class SerializedName extends \Symfony\Component\Serializer\Attribute\SerializedName
{
}
}

View File

@@ -0,0 +1,21 @@
<?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\Annotation;
class_exists(\Symfony\Component\Serializer\Attribute\SerializedPath::class);
if (false) {
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
class SerializedPath extends \Symfony\Component\Serializer\Attribute\SerializedPath
{
}
}

View File

@@ -0,0 +1,77 @@
<?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\Attribute;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
/**
* Annotation class for @Context().
*
* @Annotation
* @NamedArgumentConstructor
* @Target({"PROPERTY", "METHOD"})
*
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class Context
{
private array $groups;
/**
* @param string|string[] $groups
*
* @throws InvalidArgumentException
*/
public function __construct(
private readonly array $context = [],
private readonly array $normalizationContext = [],
private readonly array $denormalizationContext = [],
string|array $groups = [],
) {
if (!$context && !$normalizationContext && !$denormalizationContext) {
throw new InvalidArgumentException(sprintf('At least one of the "context", "normalizationContext", or "denormalizationContext" options must be provided as a non-empty array to "%s".', static::class));
}
$this->groups = (array) $groups;
foreach ($this->groups as $group) {
if (!\is_string($group)) {
throw new InvalidArgumentException(sprintf('Parameter "groups" given to "%s" must be a string or an array of strings, "%s" given.', static::class, get_debug_type($group)));
}
}
}
public function getContext(): array
{
return $this->context;
}
public function getNormalizationContext(): array
{
return $this->normalizationContext;
}
public function getDenormalizationContext(): array
{
return $this->denormalizationContext;
}
public function getGroups(): array
{
return $this->groups;
}
}
if (!class_exists(\Symfony\Component\Serializer\Annotation\Context::class, false)) {
class_alias(Context::class, \Symfony\Component\Serializer\Annotation\Context::class);
}

View File

@@ -0,0 +1,54 @@
<?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\Attribute;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
/**
* Annotation class for @DiscriminatorMap().
*
* @Annotation
* @NamedArgumentConstructor
* @Target({"CLASS"})
*
* @author Samuel Roze <samuel.roze@gmail.com>
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class DiscriminatorMap
{
public function __construct(
private readonly string $typeProperty,
private readonly array $mapping,
) {
if (empty($typeProperty)) {
throw new InvalidArgumentException(sprintf('Parameter "typeProperty" given to "%s" cannot be empty.', static::class));
}
if (empty($mapping)) {
throw new InvalidArgumentException(sprintf('Parameter "mapping" given to "%s" cannot be empty.', static::class));
}
}
public function getTypeProperty(): string
{
return $this->typeProperty;
}
public function getMapping(): array
{
return $this->mapping;
}
}
if (!class_exists(\Symfony\Component\Serializer\Annotation\DiscriminatorMap::class, false)) {
class_alias(DiscriminatorMap::class, \Symfony\Component\Serializer\Annotation\DiscriminatorMap::class);
}

View File

@@ -0,0 +1,62 @@
<?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\Attribute;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
/**
* Annotation class for @Groups().
*
* @Annotation
* @NamedArgumentConstructor
* @Target({"PROPERTY", "METHOD", "CLASS"})
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_CLASS)]
class Groups
{
/**
* @var string[]
*/
private readonly array $groups;
/**
* @param string|string[] $groups
*/
public function __construct(string|array $groups)
{
$this->groups = (array) $groups;
if (!$this->groups) {
throw new InvalidArgumentException(sprintf('Parameter given to "%s" cannot be empty.', static::class));
}
foreach ($this->groups as $group) {
if (!\is_string($group) || '' === $group) {
throw new InvalidArgumentException(sprintf('Parameter given to "%s" must be a string or an array of non-empty strings.', static::class));
}
}
}
/**
* @return string[]
*/
public function getGroups(): array
{
return $this->groups;
}
}
if (!class_exists(\Symfony\Component\Serializer\Annotation\Groups::class, false)) {
class_alias(Groups::class, \Symfony\Component\Serializer\Annotation\Groups::class);
}

View File

@@ -0,0 +1,29 @@
<?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\Attribute;
/**
* Annotation class for @Ignore().
*
* @Annotation
* @Target({"PROPERTY", "METHOD"})
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
class Ignore
{
}
if (!class_exists(\Symfony\Component\Serializer\Annotation\Ignore::class, false)) {
class_alias(Ignore::class, \Symfony\Component\Serializer\Annotation\Ignore::class);
}

View File

@@ -0,0 +1,46 @@
<?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\Attribute;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
/**
* Annotation class for @MaxDepth().
*
* @Annotation
* @NamedArgumentConstructor
* @Target({"PROPERTY", "METHOD"})
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
class MaxDepth
{
public function __construct(private readonly int $maxDepth)
{
if ($maxDepth <= 0) {
throw new InvalidArgumentException(sprintf('Parameter given to "%s" must be a positive integer.', static::class));
}
}
/**
* @return int
*/
public function getMaxDepth()
{
return $this->maxDepth;
}
}
if (!class_exists(\Symfony\Component\Serializer\Annotation\MaxDepth::class, false)) {
class_alias(MaxDepth::class, \Symfony\Component\Serializer\Annotation\MaxDepth::class);
}

View File

@@ -0,0 +1,43 @@
<?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\Attribute;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
/**
* Annotation class for @SerializedName().
*
* @Annotation
* @NamedArgumentConstructor
* @Target({"PROPERTY", "METHOD"})
*
* @author Fabien Bourigault <bourigaultfabien@gmail.com>
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
class SerializedName
{
public function __construct(private readonly string $serializedName)
{
if ('' === $serializedName) {
throw new InvalidArgumentException(sprintf('Parameter given to "%s" must be a non-empty string.', self::class));
}
}
public function getSerializedName(): string
{
return $this->serializedName;
}
}
if (!class_exists(\Symfony\Component\Serializer\Annotation\SerializedName::class, false)) {
class_alias(SerializedName::class, \Symfony\Component\Serializer\Annotation\SerializedName::class);
}

View File

@@ -0,0 +1,49 @@
<?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\Attribute;
use Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException;
use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
/**
* Annotation class for @SerializedPath().
*
* @Annotation
* @NamedArgumentConstructor
* @Target({"PROPERTY", "METHOD"})
*
* @author Tobias Bönner <tobi@boenner.family>
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
class SerializedPath
{
private PropertyPath $serializedPath;
public function __construct(string $serializedPath)
{
try {
$this->serializedPath = new PropertyPath($serializedPath);
} catch (InvalidPropertyPathException $pathException) {
throw new InvalidArgumentException(sprintf('Parameter given to "%s" must be a valid property path.', self::class));
}
}
public function getSerializedPath(): PropertyPath
{
return $this->serializedPath;
}
}
if (!class_exists(\Symfony\Component\Serializer\Annotation\SerializedPath::class, false)) {
class_alias(SerializedPath::class, \Symfony\Component\Serializer\Annotation\SerializedPath::class);
}

324
vendor/symfony/serializer/CHANGELOG.md vendored Executable file
View File

@@ -0,0 +1,324 @@
CHANGELOG
=========
6.4
---
* Add `TranslatableNormalizer`
* Allow `Context` attribute to target classes
* Deprecate Doctrine annotations support in favor of native attributes
* Allow the `Groups` attribute/annotation on classes
* JsonDecode: Add `json_decode_detailed_errors` option
* Make `ProblemNormalizer` give details about Messenger's `ValidationFailedException`
* Add `XmlEncoder::CDATA_WRAPPING` context option
* Deprecate `AnnotationLoader`, use `AttributeLoader` instead
* Add aliases for all classes in the `Annotation` namespace to `Attribute`
6.3
---
* Add `AbstractNormalizer::REQUIRE_ALL_PROPERTIES` context flag to require all properties to be listed in the input instead of falling back to null for nullable ones
* Add `XmlEncoder::SAVE_OPTIONS` context option
* Add `BackedEnumNormalizer::ALLOW_INVALID_VALUES` context option
* Add `UnsupportedFormatException` which is thrown when there is no decoder for a given format
* Add method `getSupportedTypes(?string $format)` to `NormalizerInterface` and `DenormalizerInterface`
* Make `ProblemNormalizer` give details about `ValidationFailedException` and `PartialDenormalizationException`
* Deprecate `CacheableSupportsMethodInterface` in favor of the new `getSupportedTypes(?string $format)` methods
* The following Normalizer classes will become final in 7.0:
* `ConstraintViolationListNormalizer`
* `CustomNormalizer`
* `DataUriNormalizer`
* `DateIntervalNormalizer`
* `DateTimeNormalizer`
* `DateTimeZoneNormalizer`
* `GetSetMethodNormalizer`
* `JsonSerializableNormalizer`
* `ObjectNormalizer`
* `PropertyNormalizer`
6.2
---
* Add support for constructor promoted properties to `Context` attribute
* Add context option `PropertyNormalizer::NORMALIZE_VISIBILITY` with bitmask flags `PropertyNormalizer::NORMALIZE_PUBLIC`, `PropertyNormalizer::NORMALIZE_PROTECTED`, `PropertyNormalizer::NORMALIZE_PRIVATE`
* Add method `withNormalizeVisibility` to `PropertyNormalizerContextBuilder`
* Deprecate calling `AttributeMetadata::setSerializedName()`, `ClassMetadata::setClassDiscriminatorMapping()` without arguments
* Change the signature of `AttributeMetadataInterface::setSerializedName()` to `setSerializedName(?string)`
* Change the signature of `ClassMetadataInterface::setClassDiscriminatorMapping()` to `setClassDiscriminatorMapping(?ClassDiscriminatorMapping)`
* Add option YamlEncoder::YAML_INDENTATION to YamlEncoder constructor options to configure additional indentation for each level of nesting. This allows configuring indentation in the service configuration.
* Add `SerializedPath` annotation to flatten nested attributes
6.1
---
* Add `TraceableSerializer`, `TraceableNormalizer`, `TraceableEncoder` and `SerializerDataCollector` to integrate with the web profiler
* Add the ability to create contexts using context builders
* Set `Context` annotation as not final
* Deprecate `ContextAwareNormalizerInterface`, use `NormalizerInterface` instead
* Deprecate `ContextAwareDenormalizerInterface`, use `DenormalizerInterface` instead
* Deprecate supporting denormalization for `AbstractUid` in `UidNormalizer`, use one of `AbstractUid` child class instead
* Deprecate denormalizing to an abstract class in `UidNormalizer`
* Add support for `can*()` methods to `ObjectNormalizer`
6.0
---
* Remove `ArrayDenormalizer::setSerializer()`, call `setDenormalizer()` instead
* Remove the ability to create instances of the annotation classes by passing an array of parameters, use named arguments instead
5.4
---
* Add support of PHP backed enumerations
* Add support for serializing empty array as object
* Return empty collections as `ArrayObject` from `Serializer::normalize()` when `PRESERVE_EMPTY_OBJECTS` is set
* Add support for collecting type errors during denormalization
* Add missing arguments in `MissingConstructorArgumentsException`
5.3
---
* Add the ability to provide (de)normalization context using metadata (e.g. `@Symfony\Component\Serializer\Annotation\Context`)
* Deprecate `ArrayDenormalizer::setSerializer()`, call `setDenormalizer()` instead
* Add normalization formats to `UidNormalizer`
* Add `CsvEncoder::END_OF_LINE` context option
* Deprecate creating instances of the annotation classes by passing an array of parameters, use named arguments instead
5.2.0
-----
* added `CompiledClassMetadataFactory` and `ClassMetadataFactoryCompiler` for faster metadata loading.
* added `UidNormalizer`
* added `FormErrorNormalizer`
* added `MimeMessageNormalizer`
* serializer mapping can be configured using php attributes
5.1.0
-----
* added support for scalar values denormalization
* added support for `\stdClass` to `ObjectNormalizer`
* added the ability to ignore properties using metadata (e.g. `@Symfony\Component\Serializer\Annotation\Ignore`)
* added an option to serialize constraint violations payloads (e.g. severity)
5.0.0
-----
* throw an exception when creating a `Serializer` with normalizers which neither implement `NormalizerInterface` nor `DenormalizerInterface`
* throw an exception when creating a `Serializer` with encoders which neither implement `EncoderInterface` nor `DecoderInterface`
* changed the default value of the `CsvEncoder` "as_collection" option to `true`
* removed `AbstractNormalizer::$circularReferenceLimit`, `AbstractNormalizer::$circularReferenceHandler`,
`AbstractNormalizer::$callbacks`, `AbstractNormalizer::$ignoredAttributes`,
`AbstractNormalizer::$camelizedAttributes`, `AbstractNormalizer::setCircularReferenceLimit()`,
`AbstractNormalizer::setCircularReferenceHandler()`, `AbstractNormalizer::setCallbacks()` and
`AbstractNormalizer::setIgnoredAttributes()`, use the default context instead.
* removed `AbstractObjectNormalizer::$maxDepthHandler` and `AbstractObjectNormalizer::setMaxDepthHandler()`,
use the default context instead.
* removed `XmlEncoder::setRootNodeName()` & `XmlEncoder::getRootNodeName()`, use the default context instead.
* removed individual encoders/normalizers options as constructor arguments.
* removed support for instantiating a `DataUriNormalizer` with a default MIME type guesser when the `symfony/mime` component isn't installed.
* removed the `XmlEncoder::TYPE_CASE_ATTRIBUTES` constant. Use `XmlEncoder::TYPE_CAST_ATTRIBUTES` instead.
4.4.0
-----
* deprecated the `XmlEncoder::TYPE_CASE_ATTRIBUTES` constant, use `XmlEncoder::TYPE_CAST_ATTRIBUTES` instead
* added option to output a UTF-8 BOM in CSV encoder via `CsvEncoder::OUTPUT_UTF8_BOM_KEY` context option
* added `ProblemNormalizer` to normalize errors according to the API Problem spec (RFC 7807)
4.3.0
-----
* added the list of constraint violations' parameters in `ConstraintViolationListNormalizer`
* added support for serializing `DateTimeZone` objects
* added a `deep_object_to_populate` context option to recursive denormalize on `object_to_populate` object.
4.2.0
-----
* using the default context is the new recommended way to configure normalizers and encoders
* added a `skip_null_values` context option to not serialize properties with a `null` values
* `AbstractNormalizer::handleCircularReference` is now final and receives
two optional extra arguments: the format and the context
* added support for XML comment encoding (encoding `['#comment' => ' foo ']` results `<!-- foo -->`)
* added optional `int[] $encoderIgnoredNodeTypes` argument to `XmlEncoder::__construct`
to configure node types to be ignored during encoding
* added `AdvancedNameConverterInterface` to access the class,
the format and the context in a name converter
* the `AbstractNormalizer::handleCircularReference()` method will have two new `$format`
and `$context` arguments in version 5.0, not defining them is deprecated
* deprecated creating a `Serializer` with normalizers which do not implement
either `NormalizerInterface` or `DenormalizerInterface`
* deprecated creating a `Serializer` with normalizers which do not implement
either `NormalizerInterface` or `DenormalizerInterface`
* deprecated creating a `Serializer` with encoders which do not implement
either `EncoderInterface` or `DecoderInterface`
* added the optional `$objectClassResolver` argument in `AbstractObjectNormalizer`
and `ObjectNormalizer` constructor
* added `MetadataAwareNameConverter` to configure the serialized name of properties through metadata
* `YamlEncoder` now handles the `.yml` extension too
* `AbstractNormalizer::$circularReferenceLimit`, `AbstractNormalizer::$circularReferenceHandler`,
`AbstractNormalizer::$callbacks`, `AbstractNormalizer::$ignoredAttributes`,
`AbstractNormalizer::$camelizedAttributes`, `AbstractNormalizer::setCircularReferenceLimit()`,
`AbstractNormalizer::setCircularReferenceHandler()`, `AbstractNormalizer::setCallbacks()` and
`AbstractNormalizer::setIgnoredAttributes()` are deprecated, use the default context instead.
* `AbstractObjectNormalizer::$maxDepthHandler` and `AbstractObjectNormalizer::setMaxDepthHandler()`
are deprecated, use the default context instead.
* passing configuration options directly to the constructor of `CsvEncoder`, `JsonDecode` and
`XmlEncoder` is deprecated since Symfony 4.2, use the default context instead.
4.1.0
-----
* added `CacheableSupportsMethodInterface` for normalizers and denormalizers that use
only the type and the format in their `supports*()` methods
* added `MissingConstructorArgumentsException` new exception for deserialization failure
of objects that needs data insertion in constructor
* added an optional `default_constructor_arguments` option of context to specify a default data in
case the object is not initializable by its constructor because of data missing
* added optional `bool $escapeFormulas = false` argument to `CsvEncoder::__construct`
* added `AbstractObjectNormalizer::setMaxDepthHandler` to set a handler to call when the configured
maximum depth is reached
* added optional `int[] $ignoredNodeTypes` argument to `XmlEncoder::__construct`. XML decoding now
ignores comment node types by default.
* added `ConstraintViolationListNormalizer`
4.0.0
-----
* removed the `SerializerAwareEncoder` and `SerializerAwareNormalizer` classes,
use the `SerializerAwareTrait` instead
* removed the `Serializer::$normalizerCache` and `Serializer::$denormalizerCache`
properties
* added an optional `string $format = null` argument to `AbstractNormalizer::instantiateObject`
* added an optional `array $context = []` to `Serializer::supportsNormalization`, `Serializer::supportsDenormalization`,
`Serializer::supportsEncoding` and `Serializer::supportsDecoding`
3.4.0
-----
* added `AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT` context option
to disable throwing an `UnexpectedValueException` on a type mismatch
* added support for serializing `DateInterval` objects
* added getter for extra attributes in `ExtraAttributesException`
* improved `CsvEncoder` to handle variable nested structures
* CSV headers can be passed to the `CsvEncoder` via the `csv_headers` serialization context variable
* added `$context` when checking for encoding, decoding and normalizing in `Serializer`
3.3.0
-----
* added `SerializerPass`
3.1.0
-----
* added support for serializing objects that implement `JsonSerializable`
* added the `DenormalizerAwareTrait` and `NormalizerAwareTrait` traits to
support normalizer/denormalizer awareness
* added the `DenormalizerAwareInterface` and `NormalizerAwareInterface`
interfaces to support normalizer/denormalizer awareness
* added a PSR-6 compatible adapter for caching metadata
* added a `MaxDepth` option to limit the depth of the object graph when
serializing objects
* added support for serializing `SplFileInfo` objects
* added support for serializing objects that implement `DateTimeInterface`
* added `AbstractObjectNormalizer` as a base class for normalizers that deal
with objects
* added support to relation deserialization
2.7.0
-----
* added support for serialization and deserialization groups including
annotations, XML and YAML mapping.
* added `AbstractNormalizer` to factorise code and ease normalizers development
* added circular references handling for `PropertyNormalizer`
* added support for a context key called `object_to_populate` in `AbstractNormalizer`
to reuse existing objects in the deserialization process
* added `NameConverterInterface` and `CamelCaseToSnakeCaseNameConverter`
* [DEPRECATION] `GetSetMethodNormalizer::setCamelizedAttributes()` and
`PropertyNormalizer::setCamelizedAttributes()` are replaced by
`CamelCaseToSnakeCaseNameConverter`
* [DEPRECATION] the `Exception` interface has been renamed to `ExceptionInterface`
* added `ObjectNormalizer` leveraging the `PropertyAccess` component to normalize
objects containing both properties and getters / setters / issers / hassers methods.
* added `xml_type_cast_attributes` context option for allowing users to opt-out of typecasting
xml attributes.
2.6.0
-----
* added a new serializer: `PropertyNormalizer`. Like `GetSetMethodNormalizer`,
this normalizer will map an object's properties to an array.
* added circular references handling for `GetSetMethodNormalizer`
2.5.0
-----
* added support for `is.*` getters in `GetSetMethodNormalizer`
2.4.0
-----
* added `$context` support for XMLEncoder.
* [DEPRECATION] JsonEncode and JsonDecode where modified to throw
an exception if error found. No need for `get*Error()` functions
2.3.0
-----
* added `GetSetMethodNormalizer::setCamelizedAttributes` to allow calling
camel cased methods for underscored properties
2.2.0
-----
* [BC BREAK] All Serializer, Normalizer and Encoder interfaces have been
modified to include an optional `$context` array parameter.
* The XML Root name can now be configured with the `xml_root_name`
parameter in the context option to the `XmlEncoder`.
* Options to `json_encode` and `json_decode` can be passed through
the context options of `JsonEncode` and `JsonDecode` encoder/decoders.
2.1.0
-----
* added DecoderInterface::supportsDecoding(),
EncoderInterface::supportsEncoding()
* removed NormalizableInterface::denormalize(),
NormalizerInterface::denormalize(),
NormalizerInterface::supportsDenormalization()
* removed normalize() denormalize() encode() decode() supportsSerialization()
supportsDeserialization() supportsEncoding() supportsDecoding()
getEncoder() from SerializerInterface
* Serializer now implements NormalizerInterface, DenormalizerInterface,
EncoderInterface, DecoderInterface in addition to SerializerInterface
* added DenormalizableInterface and DenormalizerInterface
* [BC BREAK] changed `GetSetMethodNormalizer`'s key names from all lowercased
to camelCased (e.g. `mypropertyvalue` to `myPropertyValue`)
* [BC BREAK] convert the `item` XML tag to an array
``` xml
<?xml version="1.0"?>
<response>
<item><title><![CDATA[title1]]></title></item><item><title><![CDATA[title2]]></title></item>
</response>
```
Before:
Array()
After:
Array(
[item] => Array(
[0] => Array(
[title] => title1
)
[1] => Array(
[title] => title2
)
)
)

View File

@@ -0,0 +1,51 @@
<?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\CacheWarmer;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryCompiler;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
/**
* @author Fabien Bourigault <bourigaultfabien@gmail.com>
*/
final class CompiledClassMetadataCacheWarmer implements CacheWarmerInterface
{
public function __construct(
private readonly array $classesToCompile,
private readonly ClassMetadataFactoryInterface $classMetadataFactory,
private readonly ClassMetadataFactoryCompiler $classMetadataFactoryCompiler,
private readonly Filesystem $filesystem,
) {
}
public function warmUp(string $cacheDir, ?string $buildDir = null): array
{
$metadatas = [];
foreach ($this->classesToCompile as $classToCompile) {
$metadatas[] = $this->classMetadataFactory->getMetadataFor($classToCompile);
}
$code = $this->classMetadataFactoryCompiler->compile($metadatas);
$this->filesystem->dumpFile("{$cacheDir}/serializer.class.metadata.php", $code);
return [];
}
public function isOptional(): bool
{
return true;
}
}

View File

@@ -0,0 +1,113 @@
<?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\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Dumper;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Serializer\Mapping\ClassMetadataInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
/**
* A console command to debug Serializer information.
*
* @author Loïc Frémont <lc.fremont@gmail.com>
*/
#[AsCommand(name: 'debug:serializer', description: 'Display serialization information for classes')]
class DebugCommand extends Command
{
public function __construct(private readonly ClassMetadataFactoryInterface $serializer)
{
parent::__construct();
}
protected function configure(): void
{
$this
->addArgument('class', InputArgument::REQUIRED, 'A fully qualified class name')
->setHelp("The <info>%command.name% 'App\Entity\Dummy'</info> command dumps the serializer groups for the dummy class.")
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$class = $input->getArgument('class');
if (!class_exists($class)) {
$io = new SymfonyStyle($input, $output);
$io->error(sprintf('Class "%s" was not found.', $class));
return Command::FAILURE;
}
$this->dumpSerializerDataForClass($input, $output, $class);
return Command::SUCCESS;
}
private function dumpSerializerDataForClass(InputInterface $input, OutputInterface $output, string $class): void
{
$io = new SymfonyStyle($input, $output);
$title = sprintf('<info>%s</info>', $class);
$rows = [];
$dump = new Dumper($output);
$classMetadata = $this->serializer->getMetadataFor($class);
foreach ($this->getAttributesData($classMetadata) as $propertyName => $data) {
$rows[] = [
$propertyName,
$dump($data),
];
}
if (!$rows) {
$io->section($title);
$io->text('No Serializer data were found for this class.');
return;
}
$io->section($title);
$table = new Table($output);
$table->setHeaders(['Property', 'Options']);
$table->setRows($rows);
$table->render();
}
/**
* @return array<string, array<string, mixed>>
*/
private function getAttributesData(ClassMetadataInterface $classMetadata): array
{
$data = [];
foreach ($classMetadata->getAttributesMetadata() as $attributeMetadata) {
$data[$attributeMetadata->getName()] = [
'groups' => $attributeMetadata->getGroups(),
'maxDepth' => $attributeMetadata->getMaxDepth(),
'serializedName' => $attributeMetadata->getSerializedName(),
'ignore' => $attributeMetadata->isIgnored(),
'normalizationContexts' => $attributeMetadata->getNormalizationContexts(),
'denormalizationContexts' => $attributeMetadata->getDenormalizationContexts(),
];
}
return $data;
}
}

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\Context;
/**
* Common interface for context builders.
*
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
* @author Robin Chalas <robin.chalas@gmail.com>
*/
interface ContextBuilderInterface
{
/**
* @param self|array<string, mixed> $context
*/
public function withContext(self|array $context): static;
/**
* @return array<string, mixed>
*/
public function toArray(): array;
}

View File

@@ -0,0 +1,54 @@
<?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\Context;
/**
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
*/
trait ContextBuilderTrait
{
/**
* @var array<string, mixed>
*/
private array $context = [];
protected function with(string $key, mixed $value): static
{
$instance = new static();
$instance->context = array_merge($this->context, [$key => $value]);
return $instance;
}
/**
* @param ContextBuilderInterface|array<string, mixed> $context
*/
public function withContext(ContextBuilderInterface|array $context): static
{
if ($context instanceof ContextBuilderInterface) {
$context = $context->toArray();
}
$instance = new static();
$instance->context = array_merge($this->context, $context);
return $instance;
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return $this->context;
}
}

View File

@@ -0,0 +1,135 @@
<?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\Context\Encoder;
use Symfony\Component\Serializer\Context\ContextBuilderInterface;
use Symfony\Component\Serializer\Context\ContextBuilderTrait;
use Symfony\Component\Serializer\Encoder\CsvEncoder;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
/**
* A helper providing autocompletion for available CsvEncoder options.
*
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
*/
final class CsvEncoderContextBuilder implements ContextBuilderInterface
{
use ContextBuilderTrait;
/**
* Configures the column delimiter character.
*
* Must be a single character.
*
* @throws InvalidArgumentException
*/
public function withDelimiter(?string $delimiter): static
{
if (null !== $delimiter && 1 !== \strlen($delimiter)) {
throw new InvalidArgumentException(sprintf('The "%s" delimiter must be a single character.', $delimiter));
}
return $this->with(CsvEncoder::DELIMITER_KEY, $delimiter);
}
/**
* Configures the field enclosure character.
*
* Must be a single character.
*
* @throws InvalidArgumentException
*/
public function withEnclosure(?string $enclosure): static
{
if (null !== $enclosure && 1 !== \strlen($enclosure)) {
throw new InvalidArgumentException(sprintf('The "%s" enclosure must be a single character.', $enclosure));
}
return $this->with(CsvEncoder::ENCLOSURE_KEY, $enclosure);
}
/**
* Configures the escape character.
*
* Must be empty or a single character.
*
* @throws InvalidArgumentException
*/
public function withEscapeChar(?string $escapeChar): static
{
if (null !== $escapeChar && \strlen($escapeChar) > 1) {
throw new InvalidArgumentException(sprintf('The "%s" escape character must be empty or a single character.', $escapeChar));
}
return $this->with(CsvEncoder::ESCAPE_CHAR_KEY, $escapeChar);
}
/**
* Configures the key separator when (un)flattening arrays.
*/
public function withKeySeparator(?string $keySeparator): static
{
return $this->with(CsvEncoder::KEY_SEPARATOR_KEY, $keySeparator);
}
/**
* Configures the headers.
*
* @param list<mixed>|null $headers
*/
public function withHeaders(?array $headers): static
{
return $this->with(CsvEncoder::HEADERS_KEY, $headers);
}
/**
* Configures whether formulas should be escaped.
*/
public function withEscapedFormulas(?bool $escapedFormulas): static
{
return $this->with(CsvEncoder::ESCAPE_FORMULAS_KEY, $escapedFormulas);
}
/**
* Configures whether the decoded result should be considered as a collection
* or as a single element.
*/
public function withAsCollection(?bool $asCollection): static
{
return $this->with(CsvEncoder::AS_COLLECTION_KEY, $asCollection);
}
/**
* Configures whether the input (or output) is containing (or will contain) headers.
*/
public function withNoHeaders(?bool $noHeaders): static
{
return $this->with(CsvEncoder::NO_HEADERS_KEY, $noHeaders);
}
/**
* Configures the end of line characters.
*/
public function withEndOfLine(?string $endOfLine): static
{
return $this->with(CsvEncoder::END_OF_LINE, $endOfLine);
}
/**
* Configures whether to add the UTF-8 Byte Order Mark (BOM)
* at the beginning of the encoded result or not.
*/
public function withOutputUtf8Bom(?bool $outputUtf8Bom): static
{
return $this->with(CsvEncoder::OUTPUT_UTF8_BOM_KEY, $outputUtf8Bom);
}
}

View File

@@ -0,0 +1,72 @@
<?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\Context\Encoder;
use Symfony\Component\Serializer\Context\ContextBuilderInterface;
use Symfony\Component\Serializer\Context\ContextBuilderTrait;
use Symfony\Component\Serializer\Encoder\JsonDecode;
use Symfony\Component\Serializer\Encoder\JsonEncode;
/**
* A helper providing autocompletion for available JsonEncoder options.
*
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
*/
final class JsonEncoderContextBuilder implements ContextBuilderInterface
{
use ContextBuilderTrait;
/**
* Configures the json_encode flags bitmask.
*
* @see https://www.php.net/manual/en/json.constants.php
*
* @param positive-int|null $options
*/
public function withEncodeOptions(?int $options): static
{
return $this->with(JsonEncode::OPTIONS, $options);
}
/**
* Configures the json_decode flags bitmask.
*
* @see https://www.php.net/manual/en/json.constants.php
*
* @param positive-int|null $options
*/
public function withDecodeOptions(?int $options): static
{
return $this->with(JsonDecode::OPTIONS, $options);
}
/**
* Configures whether decoded objects will be given as
* associative arrays or as nested stdClass.
*/
public function withAssociative(?bool $associative): static
{
return $this->with(JsonDecode::ASSOCIATIVE, $associative);
}
/**
* Configures the maximum recursion depth.
*
* Must be strictly positive.
*
* @param positive-int|null $recursionDepth
*/
public function withRecursionDepth(?int $recursionDepth): static
{
return $this->with(JsonDecode::RECURSION_DEPTH, $recursionDepth);
}
}

View File

@@ -0,0 +1,155 @@
<?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\Context\Encoder;
use Symfony\Component\Serializer\Context\ContextBuilderInterface;
use Symfony\Component\Serializer\Context\ContextBuilderTrait;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
/**
* A helper providing autocompletion for available XmlEncoder options.
*
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
*/
final class XmlEncoderContextBuilder implements ContextBuilderInterface
{
use ContextBuilderTrait;
/**
* Configures whether the decoded result should be considered as a collection
* or as a single element.
*/
public function withAsCollection(?bool $asCollection): static
{
return $this->with(XmlEncoder::AS_COLLECTION, $asCollection);
}
/**
* Configures node types to ignore while decoding.
*
* @see https://www.php.net/manual/en/dom.constants.php
*
* @param list<int>|null $decoderIgnoredNodeTypes
*/
public function withDecoderIgnoredNodeTypes(?array $decoderIgnoredNodeTypes): static
{
return $this->with(XmlEncoder::DECODER_IGNORED_NODE_TYPES, $decoderIgnoredNodeTypes);
}
/**
* Configures node types to ignore while encoding.
*
* @see https://www.php.net/manual/en/dom.constants.php
*
* @param list<int>|null $encoderIgnoredNodeTypes
*/
public function withEncoderIgnoredNodeTypes(?array $encoderIgnoredNodeTypes): static
{
return $this->with(XmlEncoder::ENCODER_IGNORED_NODE_TYPES, $encoderIgnoredNodeTypes);
}
/**
* Configures the DOMDocument encoding.
*
* @see https://www.php.net/manual/en/class.domdocument.php#domdocument.props.encoding
*/
public function withEncoding(?string $encoding): static
{
return $this->with(XmlEncoder::ENCODING, $encoding);
}
/**
* Configures whether to encode with indentation and extra space.
*
* @see https://php.net/manual/en/class.domdocument.php#domdocument.props.formatoutput
*/
public function withFormatOutput(?bool $formatOutput): static
{
return $this->with(XmlEncoder::FORMAT_OUTPUT, $formatOutput);
}
/**
* Configures the DOMDocument::loadXml options bitmask.
*
* @see https://www.php.net/manual/en/libxml.constants.php
*
* @param positive-int|null $loadOptions
*/
public function withLoadOptions(?int $loadOptions): static
{
return $this->with(XmlEncoder::LOAD_OPTIONS, $loadOptions);
}
/**
* Configures the DOMDocument::saveXml options bitmask.
*
* @see https://www.php.net/manual/en/libxml.constants.php
*
* @param positive-int|null $saveOptions
*/
public function withSaveOptions(?int $saveOptions): static
{
return $this->with(XmlEncoder::SAVE_OPTIONS, $saveOptions);
}
/**
* Configures whether to keep empty nodes.
*/
public function withRemoveEmptyTags(?bool $removeEmptyTags): static
{
return $this->with(XmlEncoder::REMOVE_EMPTY_TAGS, $removeEmptyTags);
}
/**
* Configures name of the root node.
*/
public function withRootNodeName(?string $rootNodeName): static
{
return $this->with(XmlEncoder::ROOT_NODE_NAME, $rootNodeName);
}
/**
* Configures whether the document will be standalone.
*
* @see https://php.net/manual/en/class.domdocument.php#domdocument.props.xmlstandalone
*/
public function withStandalone(?bool $standalone): static
{
return $this->with(XmlEncoder::STANDALONE, $standalone);
}
/**
* Configures whether casting numeric string attributes to integers or floats.
*/
public function withTypeCastAttributes(?bool $typeCastAttributes): static
{
return $this->with(XmlEncoder::TYPE_CAST_ATTRIBUTES, $typeCastAttributes);
}
/**
* Configures the version number of the document.
*
* @see https://php.net/manual/en/class.domdocument.php#domdocument.props.xmlversion
*/
public function withVersion(?string $version): static
{
return $this->with(XmlEncoder::VERSION, $version);
}
/**
* Configures whether to wrap strings within CDATA sections.
*/
public function withCdataWrapping(?bool $cdataWrapping): static
{
return $this->with(XmlEncoder::CDATA_WRAPPING, $cdataWrapping);
}
}

View File

@@ -0,0 +1,68 @@
<?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\Context\Encoder;
use Symfony\Component\Serializer\Context\ContextBuilderInterface;
use Symfony\Component\Serializer\Context\ContextBuilderTrait;
use Symfony\Component\Serializer\Encoder\YamlEncoder;
/**
* A helper providing autocompletion for available YamlEncoder options.
*
* Note that the "indentation" setting is not offered in this builder because
* it can only be set during the construction of the YamlEncoder, but not per
* call.
*
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
*/
final class YamlEncoderContextBuilder implements ContextBuilderInterface
{
use ContextBuilderTrait;
/**
* Configures the threshold to switch to inline YAML.
*/
public function withInlineThreshold(?int $inlineThreshold): static
{
return $this->with(YamlEncoder::YAML_INLINE, $inlineThreshold);
}
/**
* Configures the indentation level.
*
* Must be positive.
*
* @param int<0, max>|null $indentLevel
*/
public function withIndentLevel(?int $indentLevel): static
{
return $this->with(YamlEncoder::YAML_INDENT, $indentLevel);
}
/**
* Configures \Symfony\Component\Yaml\Dumper::dump flags bitmask.
*
* @see \Symfony\Component\Yaml\Yaml
*/
public function withFlags(?int $flags): static
{
return $this->with(YamlEncoder::YAML_FLAGS, $flags);
}
/**
* Configures whether to preserve empty objects "{}" or to convert them to null.
*/
public function withPreservedEmptyObjects(?bool $preserveEmptyObjects): static
{
return $this->with(YamlEncoder::PRESERVE_EMPTY_OBJECTS, $preserveEmptyObjects);
}
}

View File

@@ -0,0 +1,184 @@
<?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\Context\Normalizer;
use Symfony\Component\Serializer\Context\ContextBuilderInterface;
use Symfony\Component\Serializer\Context\ContextBuilderTrait;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
/**
* A helper providing autocompletion for available AbstractNormalizer options.
*
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
*/
abstract class AbstractNormalizerContextBuilder implements ContextBuilderInterface
{
use ContextBuilderTrait;
/**
* Configures how many loops of circular reference to allow while normalizing.
*
* The value 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.
*
* Must be strictly positive.
*
* @param positive-int|null $circularReferenceLimit
*/
public function withCircularReferenceLimit(?int $circularReferenceLimit): static
{
return $this->with(AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT, $circularReferenceLimit);
}
/**
* Configures an object to be updated instead of creating a new instance.
*
* If you have a nested structure, child objects will be overwritten with
* new instances unless you set AbstractObjectNormalizer::DEEP_OBJECT_TO_POPULATE to true.
*/
public function withObjectToPopulate(?object $objectToPopulate): static
{
return $this->with(AbstractNormalizer::OBJECT_TO_POPULATE, $objectToPopulate);
}
/**
* Configures groups containing attributes to (de)normalize.
*
* Eg: ['group1', 'group2']
*
* @param list<string>|string|null $groups
*/
public function withGroups(array|string|null $groups): static
{
if (null === $groups) {
return $this->with(AbstractNormalizer::GROUPS, null);
}
return $this->with(AbstractNormalizer::GROUPS, (array) $groups);
}
/**
* Configures attributes to (de)normalize.
*
* For nested structures, this list needs to reflect the object tree.
*
* Eg: ['foo', 'bar', 'object' => ['baz']]
*
* @param array<string|array>|null $attributes
*
* @throws InvalidArgumentException
*/
public function withAttributes(?array $attributes): static
{
$it = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($attributes ?? []), \RecursiveIteratorIterator::LEAVES_ONLY);
foreach ($it as $attribute) {
if (!\is_string($attribute)) {
throw new InvalidArgumentException(sprintf('Each attribute must be a string, "%s" given.', get_debug_type($attribute)));
}
}
return $this->with(AbstractNormalizer::ATTRIBUTES, $attributes);
}
/**
* If AbstractNormalizer::ATTRIBUTES are specified, and the source has fields that are not part of that list,
* configures whether to ignore those attributes or throw an ExtraAttributesException.
*/
public function withAllowExtraAttributes(?bool $allowExtraAttributes): static
{
return $this->with(AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES, $allowExtraAttributes);
}
/**
* Configures a hashmap of classes containing hashmaps of constructor argument => default value.
*
* The names need to match the parameter names in the constructor arguments.
*
* Eg: [Foo::class => ['foo' => true, 'bar' => 0]]
*
* @param array<class-string, array<string, mixed>>|null $defaultConstructorArguments
*/
public function withDefaultConstructorArguments(?array $defaultConstructorArguments): static
{
return $this->with(AbstractNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS, $defaultConstructorArguments);
}
/**
* Deprecated in Symfony 7.1, use withDefaultConstructorArguments() instead.
*/
public function withDefaultContructorArguments(?array $defaultContructorArguments): static
{
return self::withDefaultConstructorArguments($defaultContructorArguments);
}
/**
* Configures an hashmap of field name => callable to normalize this field.
*
* The callable is called if the field is encountered with the arguments:
*
* - 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<string, mixed> $context the serialization context
*
* @param array<string, callable>|null $callbacks
*/
public function withCallbacks(?array $callbacks): static
{
return $this->with(AbstractNormalizer::CALLBACKS, $callbacks);
}
/**
* Configures an handler to call when a circular reference has been detected.
*
* If no handler is specified, 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 function withCircularReferenceHandler(?callable $circularReferenceHandler): static
{
return $this->with(AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER, $circularReferenceHandler);
}
/**
* Configures attributes to be skipped when normalizing an object tree.
*
* This list is applied to each element of nested structures.
*
* Eg: ['foo', 'bar']
*
* Note: The behaviour for nested structures is different from ATTRIBUTES
* for historical reason. Aligning the behaviour would be a BC break.
*
* @param list<string>|null $ignoredAttributes
*/
public function withIgnoredAttributes(?array $ignoredAttributes): static
{
return $this->with(AbstractNormalizer::IGNORED_ATTRIBUTES, $ignoredAttributes);
}
/**
* Configures requiring all properties to be listed in the input instead
* of falling back to null for nullable ones.
*/
public function withRequireAllProperties(?bool $requireAllProperties = true): static
{
return $this->with(AbstractNormalizer::REQUIRE_ALL_PROPERTIES, $requireAllProperties);
}
}

View File

@@ -0,0 +1,131 @@
<?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\Context\Normalizer;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
/**
* A helper providing autocompletion for available AbstractObjectNormalizer options.
*
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
*/
abstract class AbstractObjectNormalizerContextBuilder extends AbstractNormalizerContextBuilder
{
/**
* Configures whether to respect the max depth metadata on fields.
*/
public function withEnableMaxDepth(?bool $enableMaxDepth): static
{
return $this->with(AbstractObjectNormalizer::ENABLE_MAX_DEPTH, $enableMaxDepth);
}
/**
* Configures a pattern to keep track of the current depth.
*
* Must contain exactly two string placeholders.
*
* @throws InvalidArgumentException
*/
public function withDepthKeyPattern(?string $depthKeyPattern): static
{
if (null === $depthKeyPattern) {
return $this->with(AbstractObjectNormalizer::DEPTH_KEY_PATTERN, null);
}
// This will match every occurrences of sprintf specifiers
$matches = [];
preg_match_all('/(?<!%)(?:%{2})*%(?<specifier>[a-z])/', $depthKeyPattern, $matches);
if (2 !== \count($matches['specifier']) || 's' !== $matches['specifier'][0] || 's' !== $matches['specifier'][1]) {
throw new InvalidArgumentException(sprintf('The depth key pattern "%s" is not valid. You must set exactly two string placeholders.', $depthKeyPattern));
}
return $this->with(AbstractObjectNormalizer::DEPTH_KEY_PATTERN, $depthKeyPattern);
}
/**
* Configures whether verifying types match during denormalization.
*/
public function withDisableTypeEnforcement(?bool $disableTypeEnforcement): static
{
return $this->with(AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT, $disableTypeEnforcement);
}
/**
* Configures whether fields with the value `null` should be output during normalization.
*/
public function withSkipNullValues(?bool $skipNullValues): static
{
return $this->with(AbstractObjectNormalizer::SKIP_NULL_VALUES, $skipNullValues);
}
/**
* Configures whether uninitialized typed class properties should be excluded during normalization.
*/
public function withSkipUninitializedValues(?bool $skipUninitializedValues): static
{
return $this->with(AbstractObjectNormalizer::SKIP_UNINITIALIZED_VALUES, $skipUninitializedValues);
}
/**
* Configures a 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<string, mixed> $context the serialization context
*/
public function withMaxDepthHandler(?callable $maxDepthHandler): static
{
return $this->with(AbstractObjectNormalizer::MAX_DEPTH_HANDLER, $maxDepthHandler);
}
/**
* Configures which context key are not relevant to determine which attributes
* of an object to (de)normalize.
*
* @param list<string>|null $excludeFromCacheKeys
*/
public function withExcludeFromCacheKeys(?array $excludeFromCacheKeys): static
{
return $this->with(AbstractObjectNormalizer::EXCLUDE_FROM_CACHE_KEY, $excludeFromCacheKeys);
}
/**
* Configures whether 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 AbstractNormalizer::OBJECT_TO_POPULATE.
*/
public function withDeepObjectToPopulate(?bool $deepObjectToPopulate): static
{
return $this->with(AbstractObjectNormalizer::DEEP_OBJECT_TO_POPULATE, $deepObjectToPopulate);
}
/**
* Configures whether an empty object should be kept as an object (in
* JSON: {}) or converted to a list (in JSON: []).
*/
public function withPreserveEmptyObjects(?bool $preserveEmptyObjects): static
{
return $this->with(AbstractObjectNormalizer::PRESERVE_EMPTY_OBJECTS, $preserveEmptyObjects);
}
}

View File

@@ -0,0 +1,35 @@
<?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\Context\Normalizer;
use Symfony\Component\Serializer\Context\ContextBuilderInterface;
use Symfony\Component\Serializer\Context\ContextBuilderTrait;
use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer;
/**
* A helper providing autocompletion for available BackedEnumNormalizer options.
*
* @author Nicolas PHILIPPE <nikophil@gmail.com>
*/
final class BackedEnumNormalizerContextBuilder implements ContextBuilderInterface
{
use ContextBuilderTrait;
/**
* Configures if invalid values are allowed in denormalization.
* They will be denormalized into `null` values.
*/
public function withAllowInvalidValues(bool $allowInvalidValues): static
{
return $this->with(BackedEnumNormalizer::ALLOW_INVALID_VALUES, $allowInvalidValues);
}
}

View File

@@ -0,0 +1,71 @@
<?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\Context\Normalizer;
use Symfony\Component\Serializer\Context\ContextBuilderInterface;
use Symfony\Component\Serializer\Context\ContextBuilderTrait;
use Symfony\Component\Serializer\Normalizer\ConstraintViolationListNormalizer;
/**
* A helper providing autocompletion for available ConstraintViolationList options.
*
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
*/
final class ConstraintViolationListNormalizerContextBuilder implements ContextBuilderInterface
{
use ContextBuilderTrait;
/**
* Configure the instance field of normalized data.
*/
public function withInstance(mixed $instance): static
{
return $this->with(ConstraintViolationListNormalizer::INSTANCE, $instance);
}
/**
* Configure the status field of normalized data.
*/
public function withStatus(?int $status): static
{
return $this->with(ConstraintViolationListNormalizer::STATUS, $status);
}
/**
* Configure the title field of normalized data.
*/
public function withTitle(?string $title): static
{
return $this->with(ConstraintViolationListNormalizer::TITLE, $title);
}
/**
* Configure the type field of normalized data.
*/
public function withType(?string $type): static
{
return $this->with(ConstraintViolationListNormalizer::TYPE, $type);
}
/**
* Configures the payload fields which will act as an allowlist
* for the payload field of normalized data.
*
* Eg: ['foo', 'bar']
*
* @param list<string>|null $payloadFields
*/
public function withPayloadFields(?array $payloadFields): static
{
return $this->with(ConstraintViolationListNormalizer::PAYLOAD_FIELDS, $payloadFields);
}
}

View File

@@ -0,0 +1,36 @@
<?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\Context\Normalizer;
use Symfony\Component\Serializer\Context\ContextBuilderInterface;
use Symfony\Component\Serializer\Context\ContextBuilderTrait;
use Symfony\Component\Serializer\Normalizer\DateIntervalNormalizer;
/**
* A helper providing autocompletion for available DateIntervalNormalizer options.
*
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
*/
final class DateIntervalNormalizerContextBuilder implements ContextBuilderInterface
{
use ContextBuilderTrait;
/**
* Configures the format of the interval.
*
* @see https://php.net/manual/en/dateinterval.format.php
*/
public function withFormat(?string $format): static
{
return $this->with(DateIntervalNormalizer::FORMAT_KEY, $format);
}
}

View File

@@ -0,0 +1,64 @@
<?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\Context\Normalizer;
use Symfony\Component\Serializer\Context\ContextBuilderInterface;
use Symfony\Component\Serializer\Context\ContextBuilderTrait;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
/**
* A helper providing autocompletion for available DateTimeNormalizer options.
*
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
*/
final class DateTimeNormalizerContextBuilder implements ContextBuilderInterface
{
use ContextBuilderTrait;
/**
* Configures the format of the date.
*
* @see https://secure.php.net/manual/en/datetime.format.php
*/
public function withFormat(?string $format): static
{
return $this->with(DateTimeNormalizer::FORMAT_KEY, $format);
}
/**
* Configures the timezone of the date.
*
* It could be either a \DateTimeZone or a string
* that will be used to construct the \DateTimeZone
*
* @see https://secure.php.net/manual/en/class.datetimezone.php
*
* @throws InvalidArgumentException
*/
public function withTimezone(\DateTimeZone|string|null $timezone): static
{
if (null === $timezone) {
return $this->with(DateTimeNormalizer::TIMEZONE_KEY, null);
}
if (\is_string($timezone)) {
try {
$timezone = new \DateTimeZone($timezone);
} catch (\Exception $e) {
throw new InvalidArgumentException(sprintf('The "%s" timezone is invalid.', $timezone), previous: $e);
}
}
return $this->with(DateTimeNormalizer::TIMEZONE_KEY, $timezone);
}
}

View File

@@ -0,0 +1,50 @@
<?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\Context\Normalizer;
use Symfony\Component\Serializer\Context\ContextBuilderInterface;
use Symfony\Component\Serializer\Context\ContextBuilderTrait;
use Symfony\Component\Serializer\Normalizer\FormErrorNormalizer;
/**
* A helper providing autocompletion for available FormErrorNormalizer options.
*
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
*/
final class FormErrorNormalizerContextBuilder implements ContextBuilderInterface
{
use ContextBuilderTrait;
/**
* Configures the title of the normalized data.
*/
public function withTitle(?string $title): static
{
return $this->with(FormErrorNormalizer::TITLE, $title);
}
/**
* Configures the type of the normalized data.
*/
public function withType(?string $type): static
{
return $this->with(FormErrorNormalizer::TYPE, $type);
}
/**
* Configures the code of the normalized data.
*/
public function withStatusCode(?int $statusCode): static
{
return $this->with(FormErrorNormalizer::CODE, $statusCode);
}
}

View File

@@ -0,0 +1,21 @@
<?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\Context\Normalizer;
/**
* A helper providing autocompletion for available GetSetMethodNormalizer options.
*
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
*/
final class GetSetMethodNormalizerContextBuilder extends AbstractObjectNormalizerContextBuilder
{
}

View File

@@ -0,0 +1,21 @@
<?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\Context\Normalizer;
/**
* A helper providing autocompletion for available JsonSerializableNormalizer options.
*
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
*/
final class JsonSerializableNormalizerContextBuilder extends AbstractNormalizerContextBuilder
{
}

View File

@@ -0,0 +1,21 @@
<?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\Context\Normalizer;
/**
* A helper providing autocompletion for available ObjectNormalizer options.
*
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
*/
final class ObjectNormalizerContextBuilder extends AbstractObjectNormalizerContextBuilder
{
}

View File

@@ -0,0 +1,50 @@
<?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\Context\Normalizer;
use Symfony\Component\Serializer\Context\ContextBuilderInterface;
use Symfony\Component\Serializer\Context\ContextBuilderTrait;
use Symfony\Component\Serializer\Normalizer\ProblemNormalizer;
/**
* A helper providing autocompletion for available ProblemNormalizer options.
*
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
*/
final class ProblemNormalizerContextBuilder implements ContextBuilderInterface
{
use ContextBuilderTrait;
/**
* Configure the title field of normalized data.
*/
public function withTitle(?string $title): static
{
return $this->with(ProblemNormalizer::TITLE, $title);
}
/**
* Configure the type field of normalized data.
*/
public function withType(?string $type): static
{
return $this->with(ProblemNormalizer::TYPE, $type);
}
/**
* Configure the status field of normalized data.
*/
public function withStatusCode(int|string|null $statusCode): static
{
return $this->with(ProblemNormalizer::STATUS, $statusCode);
}
}

View File

@@ -0,0 +1,30 @@
<?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\Context\Normalizer;
use Symfony\Component\Serializer\Normalizer\PropertyNormalizer;
/**
* A helper providing autocompletion for available PropertyNormalizer options.
*
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
*/
final class PropertyNormalizerContextBuilder extends AbstractObjectNormalizerContextBuilder
{
/**
* Configures whether fields should be output based on visibility.
*/
public function withNormalizeVisibility(int $normalizeVisibility): static
{
return $this->with(PropertyNormalizer::NORMALIZE_VISIBILITY, $normalizeVisibility);
}
}

View File

@@ -0,0 +1,41 @@
<?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\Context\Normalizer;
use Symfony\Component\Serializer\Context\ContextBuilderInterface;
use Symfony\Component\Serializer\Context\ContextBuilderTrait;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Normalizer\UidNormalizer;
/**
* A helper providing autocompletion for available UidNormalizer options.
*
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
*/
final class UidNormalizerContextBuilder implements ContextBuilderInterface
{
use ContextBuilderTrait;
/**
* Configures the uuid format for normalization.
*
* @throws InvalidArgumentException
*/
public function withNormalizationFormat(?string $normalizationFormat): static
{
if (null !== $normalizationFormat && !\in_array($normalizationFormat, UidNormalizer::NORMALIZATION_FORMATS, true)) {
throw new InvalidArgumentException(sprintf('The "%s" normalization format is not valid.', $normalizationFormat));
}
return $this->with(UidNormalizer::NORMALIZATION_FORMAT_KEY, $normalizationFormat);
}
}

View File

@@ -0,0 +1,53 @@
<?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\Context\Normalizer;
use Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException;
use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Component\Serializer\Context\ContextBuilderInterface;
use Symfony\Component\Serializer\Context\ContextBuilderTrait;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Normalizer\UnwrappingDenormalizer;
/**
* A helper providing autocompletion for available UnwrappingDenormalizer options.
*
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
*/
final class UnwrappingDenormalizerContextBuilder implements ContextBuilderInterface
{
use ContextBuilderTrait;
/**
* Configures the path of wrapped data during denormalization.
*
* Eg: [foo].bar[bar]
*
* @see https://symfony.com/doc/current/components/property_access.html
*
* @throws InvalidArgumentException
*/
public function withUnwrapPath(?string $unwrapPath): static
{
if (null === $unwrapPath) {
return $this->with(UnwrappingDenormalizer::UNWRAP_PATH, null);
}
try {
new PropertyPath($unwrapPath);
} catch (InvalidPropertyPathException $e) {
throw new InvalidArgumentException(sprintf('The "%s" property path is not valid.', $unwrapPath), previous: $e);
}
return $this->with(UnwrappingDenormalizer::UNWRAP_PATH, $unwrapPath);
}
}

View File

@@ -0,0 +1,39 @@
<?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\Context;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Serializer;
/**
* A helper providing autocompletion for available Serializer options.
*
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
*/
final class SerializerContextBuilder implements ContextBuilderInterface
{
use ContextBuilderTrait;
/**
* Configures whether an empty array should be transformed to an
* object (in JSON: {}) or to a list (in JSON: []).
*/
public function withEmptyArrayAsObject(?bool $emptyArrayAsObject): static
{
return $this->with(Serializer::EMPTY_ARRAY_AS_OBJECT, $emptyArrayAsObject);
}
public function withCollectDenormalizationErrors(?bool $collectDenormalizationErrors): static
{
return $this->with(DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS, $collectDenormalizationErrors);
}
}

View File

@@ -0,0 +1,233 @@
<?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\DataCollector;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
use Symfony\Component\Serializer\Debug\TraceableSerializer;
use Symfony\Component\VarDumper\Cloner\Data;
/**
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
*
* @internal
*/
class SerializerDataCollector extends DataCollector implements LateDataCollectorInterface
{
private array $collected = [];
public function reset(): void
{
$this->data = [];
$this->collected = [];
}
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
{
// Everything is collected during the request, and formatted on kernel terminate.
}
public function getName(): string
{
return 'serializer';
}
public function getData(): Data|array
{
return $this->data;
}
public function getHandledCount(): int
{
return array_sum(array_map('count', $this->data));
}
public function getTotalTime(): float
{
$totalTime = 0;
foreach ($this->data as $handled) {
$totalTime += array_sum(array_map(fn (array $el): float => $el['time'], $handled));
}
return $totalTime;
}
public function collectSerialize(string $traceId, mixed $data, string $format, array $context, float $time, array $caller): void
{
unset($context[TraceableSerializer::DEBUG_TRACE_ID]);
$this->collected[$traceId] = array_merge(
$this->collected[$traceId] ?? [],
compact('data', 'format', 'context', 'time', 'caller'),
['method' => 'serialize'],
);
}
public function collectDeserialize(string $traceId, mixed $data, string $type, string $format, array $context, float $time, array $caller): void
{
unset($context[TraceableSerializer::DEBUG_TRACE_ID]);
$this->collected[$traceId] = array_merge(
$this->collected[$traceId] ?? [],
compact('data', 'format', 'type', 'context', 'time', 'caller'),
['method' => 'deserialize'],
);
}
public function collectNormalize(string $traceId, mixed $data, ?string $format, array $context, float $time, array $caller): void
{
unset($context[TraceableSerializer::DEBUG_TRACE_ID]);
$this->collected[$traceId] = array_merge(
$this->collected[$traceId] ?? [],
compact('data', 'format', 'context', 'time', 'caller'),
['method' => 'normalize'],
);
}
public function collectDenormalize(string $traceId, mixed $data, string $type, ?string $format, array $context, float $time, array $caller): void
{
unset($context[TraceableSerializer::DEBUG_TRACE_ID]);
$this->collected[$traceId] = array_merge(
$this->collected[$traceId] ?? [],
compact('data', 'format', 'type', 'context', 'time', 'caller'),
['method' => 'denormalize'],
);
}
public function collectEncode(string $traceId, mixed $data, ?string $format, array $context, float $time, array $caller): void
{
unset($context[TraceableSerializer::DEBUG_TRACE_ID]);
$this->collected[$traceId] = array_merge(
$this->collected[$traceId] ?? [],
compact('data', 'format', 'context', 'time', 'caller'),
['method' => 'encode'],
);
}
public function collectDecode(string $traceId, mixed $data, ?string $format, array $context, float $time, array $caller): void
{
unset($context[TraceableSerializer::DEBUG_TRACE_ID]);
$this->collected[$traceId] = array_merge(
$this->collected[$traceId] ?? [],
compact('data', 'format', 'context', 'time', 'caller'),
['method' => 'decode'],
);
}
public function collectNormalization(string $traceId, string $normalizer, float $time): void
{
$method = 'normalize';
$this->collected[$traceId]['normalization'][] = compact('normalizer', 'method', 'time');
}
public function collectDenormalization(string $traceId, string $normalizer, float $time): void
{
$method = 'denormalize';
$this->collected[$traceId]['normalization'][] = compact('normalizer', 'method', 'time');
}
public function collectEncoding(string $traceId, string $encoder, float $time): void
{
$method = 'encode';
$this->collected[$traceId]['encoding'][] = compact('encoder', 'method', 'time');
}
public function collectDecoding(string $traceId, string $encoder, float $time): void
{
$method = 'decode';
$this->collected[$traceId]['encoding'][] = compact('encoder', 'method', 'time');
}
public function lateCollect(): void
{
$this->data = [
'serialize' => [],
'deserialize' => [],
'normalize' => [],
'denormalize' => [],
'encode' => [],
'decode' => [],
];
foreach ($this->collected as $collected) {
if (!isset($collected['data'])) {
continue;
}
$data = [
'data' => $this->cloneVar($collected['data']),
'dataType' => get_debug_type($collected['data']),
'type' => $collected['type'] ?? null,
'format' => $collected['format'],
'time' => $collected['time'],
'context' => $this->cloneVar($collected['context']),
'normalization' => [],
'encoding' => [],
'caller' => $collected['caller'] ?? null,
];
if (isset($collected['normalization'])) {
$mainNormalization = array_pop($collected['normalization']);
$data['normalizer'] = ['time' => $mainNormalization['time']] + $this->getMethodLocation($mainNormalization['normalizer'], $mainNormalization['method']);
foreach ($collected['normalization'] as $normalization) {
if (!isset($data['normalization'][$normalization['normalizer']])) {
$data['normalization'][$normalization['normalizer']] = ['time' => 0, 'calls' => 0] + $this->getMethodLocation($normalization['normalizer'], $normalization['method']);
}
++$data['normalization'][$normalization['normalizer']]['calls'];
$data['normalization'][$normalization['normalizer']]['time'] += $normalization['time'];
}
}
if (isset($collected['encoding'])) {
$mainEncoding = array_pop($collected['encoding']);
$data['encoder'] = ['time' => $mainEncoding['time']] + $this->getMethodLocation($mainEncoding['encoder'], $mainEncoding['method']);
foreach ($collected['encoding'] as $encoding) {
if (!isset($data['encoding'][$encoding['encoder']])) {
$data['encoding'][$encoding['encoder']] = ['time' => 0, 'calls' => 0] + $this->getMethodLocation($encoding['encoder'], $encoding['method']);
}
++$data['encoding'][$encoding['encoder']]['calls'];
$data['encoding'][$encoding['encoder']]['time'] += $encoding['time'];
}
}
$this->data[$collected['method']][] = $data;
}
}
private function getMethodLocation(string $class, string $method): array
{
$reflection = new \ReflectionClass($class);
return [
'class' => $reflection->getShortName(),
'file' => $reflection->getFileName(),
'line' => $reflection->getMethod($method)->getStartLine(),
];
}
}

View File

@@ -0,0 +1,109 @@
<?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\Debug;
use Symfony\Component\Serializer\DataCollector\SerializerDataCollector;
use Symfony\Component\Serializer\Encoder\DecoderInterface;
use Symfony\Component\Serializer\Encoder\EncoderInterface;
use Symfony\Component\Serializer\Encoder\NormalizationAwareInterface;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerInterface;
/**
* Collects some data about encoding.
*
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
*
* @internal
*/
class TraceableEncoder implements EncoderInterface, DecoderInterface, SerializerAwareInterface
{
public function __construct(
private EncoderInterface|DecoderInterface $encoder,
private SerializerDataCollector $dataCollector,
) {
}
public function encode(mixed $data, string $format, array $context = []): string
{
if (!$this->encoder instanceof EncoderInterface) {
throw new \BadMethodCallException(sprintf('The "%s()" method cannot be called as nested encoder doesn\'t implements "%s".', __METHOD__, EncoderInterface::class));
}
$startTime = microtime(true);
$encoded = $this->encoder->encode($data, $format, $context);
$time = microtime(true) - $startTime;
if ($traceId = ($context[TraceableSerializer::DEBUG_TRACE_ID] ?? null)) {
$this->dataCollector->collectEncoding($traceId, $this->encoder::class, $time);
}
return $encoded;
}
public function supportsEncoding(string $format, array $context = []): bool
{
if (!$this->encoder instanceof EncoderInterface) {
return false;
}
return $this->encoder->supportsEncoding($format, $context);
}
public function decode(string $data, string $format, array $context = []): mixed
{
if (!$this->encoder instanceof DecoderInterface) {
throw new \BadMethodCallException(sprintf('The "%s()" method cannot be called as nested encoder doesn\'t implements "%s".', __METHOD__, DecoderInterface::class));
}
$startTime = microtime(true);
$encoded = $this->encoder->decode($data, $format, $context);
$time = microtime(true) - $startTime;
if ($traceId = ($context[TraceableSerializer::DEBUG_TRACE_ID] ?? null)) {
$this->dataCollector->collectDecoding($traceId, $this->encoder::class, $time);
}
return $encoded;
}
public function supportsDecoding(string $format, array $context = []): bool
{
if (!$this->encoder instanceof DecoderInterface) {
return false;
}
return $this->encoder->supportsDecoding($format, $context);
}
public function setSerializer(SerializerInterface $serializer): void
{
if (!$this->encoder instanceof SerializerAwareInterface) {
return;
}
$this->encoder->setSerializer($serializer);
}
public function needsNormalization(): bool
{
return !$this->encoder instanceof NormalizationAwareInterface;
}
/**
* Proxies all method calls to the original encoder.
*/
public function __call(string $method, array $arguments): mixed
{
return $this->encoder->{$method}(...$arguments);
}
}

View File

@@ -0,0 +1,147 @@
<?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\Debug;
use Symfony\Component\Serializer\DataCollector\SerializerDataCollector;
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerInterface;
/**
* Collects some data about normalization.
*
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
*
* @internal
*/
class TraceableNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface, NormalizerAwareInterface, DenormalizerAwareInterface, CacheableSupportsMethodInterface
{
public function __construct(
private NormalizerInterface|DenormalizerInterface $normalizer,
private SerializerDataCollector $dataCollector,
) {
if (!method_exists($normalizer, 'getSupportedTypes')) {
trigger_deprecation('symfony/serializer', '6.3', 'Not implementing the "NormalizerInterface::getSupportedTypes()" in "%s" is deprecated.', get_debug_type($normalizer));
}
}
public function getSupportedTypes(?string $format): array
{
// @deprecated remove condition in 7.0
if (!method_exists($this->normalizer, 'getSupportedTypes')) {
return ['*' => $this->normalizer instanceof CacheableSupportsMethodInterface && $this->normalizer->hasCacheableSupportsMethod()];
}
return $this->normalizer->getSupportedTypes($format);
}
public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
{
if (!$this->normalizer instanceof NormalizerInterface) {
throw new \BadMethodCallException(sprintf('The "%s()" method cannot be called as nested normalizer doesn\'t implements "%s".', __METHOD__, NormalizerInterface::class));
}
$startTime = microtime(true);
$normalized = $this->normalizer->normalize($object, $format, $context);
$time = microtime(true) - $startTime;
if ($traceId = ($context[TraceableSerializer::DEBUG_TRACE_ID] ?? null)) {
$this->dataCollector->collectNormalization($traceId, $this->normalizer::class, $time);
}
return $normalized;
}
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
if (!$this->normalizer instanceof NormalizerInterface) {
return false;
}
return $this->normalizer->supportsNormalization($data, $format, $context);
}
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed
{
if (!$this->normalizer instanceof DenormalizerInterface) {
throw new \BadMethodCallException(sprintf('The "%s()" method cannot be called as nested normalizer doesn\'t implements "%s".', __METHOD__, DenormalizerInterface::class));
}
$startTime = microtime(true);
$denormalized = $this->normalizer->denormalize($data, $type, $format, $context);
$time = microtime(true) - $startTime;
if ($traceId = ($context[TraceableSerializer::DEBUG_TRACE_ID] ?? null)) {
$this->dataCollector->collectDenormalization($traceId, $this->normalizer::class, $time);
}
return $denormalized;
}
public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
{
if (!$this->normalizer instanceof DenormalizerInterface) {
return false;
}
return $this->normalizer->supportsDenormalization($data, $type, $format, $context);
}
public function setSerializer(SerializerInterface $serializer): void
{
if (!$this->normalizer instanceof SerializerAwareInterface) {
return;
}
$this->normalizer->setSerializer($serializer);
}
public function setNormalizer(NormalizerInterface $normalizer): void
{
if (!$this->normalizer instanceof NormalizerAwareInterface) {
return;
}
$this->normalizer->setNormalizer($normalizer);
}
public function setDenormalizer(DenormalizerInterface $denormalizer): void
{
if (!$this->normalizer instanceof DenormalizerAwareInterface) {
return;
}
$this->normalizer->setDenormalizer($denormalizer);
}
/**
* @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->normalizer));
return $this->normalizer instanceof CacheableSupportsMethodInterface && $this->normalizer->hasCacheableSupportsMethod();
}
/**
* Proxies all method calls to the original normalizer.
*/
public function __call(string $method, array $arguments): mixed
{
return $this->normalizer->{$method}(...$arguments);
}
}

View File

@@ -0,0 +1,194 @@
<?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\Debug;
use Symfony\Component\Serializer\DataCollector\SerializerDataCollector;
use Symfony\Component\Serializer\Encoder\DecoderInterface;
use Symfony\Component\Serializer\Encoder\EncoderInterface;
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerInterface;
/**
* Collects some data about serialization.
*
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
*
* @internal
*/
class TraceableSerializer implements SerializerInterface, NormalizerInterface, DenormalizerInterface, EncoderInterface, DecoderInterface
{
public const DEBUG_TRACE_ID = 'debug_trace_id';
public function __construct(
private SerializerInterface&NormalizerInterface&DenormalizerInterface&EncoderInterface&DecoderInterface $serializer,
private SerializerDataCollector $dataCollector,
) {
if (!method_exists($serializer, 'getSupportedTypes')) {
trigger_deprecation('symfony/serializer', '6.3', 'Not implementing the "NormalizerInterface::getSupportedTypes()" in "%s" is deprecated.', get_debug_type($serializer));
}
}
public function serialize(mixed $data, string $format, array $context = []): string
{
$context[self::DEBUG_TRACE_ID] = $traceId = uniqid();
$startTime = microtime(true);
$result = $this->serializer->serialize($data, $format, $context);
$time = microtime(true) - $startTime;
$caller = $this->getCaller(__FUNCTION__, SerializerInterface::class);
$this->dataCollector->collectSerialize($traceId, $data, $format, $context, $time, $caller);
return $result;
}
public function deserialize(mixed $data, string $type, string $format, array $context = []): mixed
{
$context[self::DEBUG_TRACE_ID] = $traceId = uniqid();
$startTime = microtime(true);
$result = $this->serializer->deserialize($data, $type, $format, $context);
$time = microtime(true) - $startTime;
$caller = $this->getCaller(__FUNCTION__, SerializerInterface::class);
$this->dataCollector->collectDeserialize($traceId, $data, $type, $format, $context, $time, $caller);
return $result;
}
public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
{
$context[self::DEBUG_TRACE_ID] = $traceId = uniqid();
$startTime = microtime(true);
$result = $this->serializer->normalize($object, $format, $context);
$time = microtime(true) - $startTime;
$caller = $this->getCaller(__FUNCTION__, NormalizerInterface::class);
$this->dataCollector->collectNormalize($traceId, $object, $format, $context, $time, $caller);
return $result;
}
public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed
{
$context[self::DEBUG_TRACE_ID] = $traceId = uniqid();
$startTime = microtime(true);
$result = $this->serializer->denormalize($data, $type, $format, $context);
$time = microtime(true) - $startTime;
$caller = $this->getCaller(__FUNCTION__, DenormalizerInterface::class);
$this->dataCollector->collectDenormalize($traceId, $data, $type, $format, $context, $time, $caller);
return $result;
}
public function encode(mixed $data, string $format, array $context = []): string
{
$context[self::DEBUG_TRACE_ID] = $traceId = uniqid();
$startTime = microtime(true);
$result = $this->serializer->encode($data, $format, $context);
$time = microtime(true) - $startTime;
$caller = $this->getCaller(__FUNCTION__, EncoderInterface::class);
$this->dataCollector->collectEncode($traceId, $data, $format, $context, $time, $caller);
return $result;
}
public function decode(string $data, string $format, array $context = []): mixed
{
$context[self::DEBUG_TRACE_ID] = $traceId = uniqid();
$startTime = microtime(true);
$result = $this->serializer->decode($data, $format, $context);
$time = microtime(true) - $startTime;
$caller = $this->getCaller(__FUNCTION__, DecoderInterface::class);
$this->dataCollector->collectDecode($traceId, $data, $format, $context, $time, $caller);
return $result;
}
public function getSupportedTypes(?string $format): array
{
// @deprecated remove condition in 7.0
if (!method_exists($this->serializer, 'getSupportedTypes')) {
return ['*' => $this->serializer instanceof CacheableSupportsMethodInterface && $this->serializer->hasCacheableSupportsMethod()];
}
return $this->serializer->getSupportedTypes($format);
}
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
return $this->serializer->supportsNormalization($data, $format, $context);
}
public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
{
return $this->serializer->supportsDenormalization($data, $type, $format, $context);
}
public function supportsEncoding(string $format, array $context = []): bool
{
return $this->serializer->supportsEncoding($format, $context);
}
public function supportsDecoding(string $format, array $context = []): bool
{
return $this->serializer->supportsDecoding($format, $context);
}
/**
* Proxies all method calls to the original serializer.
*/
public function __call(string $method, array $arguments): mixed
{
return $this->serializer->{$method}(...$arguments);
}
private function getCaller(string $method, string $interface): array
{
$trace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 8);
$file = $trace[0]['file'];
$line = $trace[0]['line'];
for ($i = 1; $i < 8; ++$i) {
if (isset($trace[$i]['class'], $trace[$i]['function'])
&& $method === $trace[$i]['function']
&& is_a($trace[$i]['class'], $interface, true)
) {
$file = $trace[$i]['file'];
$line = $trace[$i]['line'];
break;
}
}
$name = str_replace('\\', '/', $file);
$name = substr($name, strrpos($name, '/') + 1);
return compact('name', 'file', 'line');
}
}

View File

@@ -0,0 +1,77 @@
<?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\DependencyInjection;
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Serializer\Debug\TraceableEncoder;
use Symfony\Component\Serializer\Debug\TraceableNormalizer;
/**
* Adds all services with the tags "serializer.encoder" and "serializer.normalizer" as
* encoders and normalizers to the "serializer" service.
*
* @author Javier Lopez <f12loalf@gmail.com>
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class SerializerPass implements CompilerPassInterface
{
use PriorityTaggedServiceTrait;
/**
* @return void
*/
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition('serializer')) {
return;
}
if (!$normalizers = $this->findAndSortTaggedServices('serializer.normalizer', $container)) {
throw new RuntimeException('You must tag at least one service as "serializer.normalizer" to use the "serializer" service.');
}
if (!$encoders = $this->findAndSortTaggedServices('serializer.encoder', $container)) {
throw new RuntimeException('You must tag at least one service as "serializer.encoder" to use the "serializer" service.');
}
if ($container->hasParameter('serializer.default_context')) {
$defaultContext = $container->getParameter('serializer.default_context');
foreach (array_merge($normalizers, $encoders) as $service) {
$definition = $container->getDefinition($service);
$definition->setBindings(['array $defaultContext' => new BoundArgument($defaultContext, false)] + $definition->getBindings());
}
$container->getParameterBag()->remove('serializer.default_context');
}
if ($container->getParameter('kernel.debug') && $container->hasDefinition('serializer.data_collector')) {
foreach ($normalizers as $i => $normalizer) {
$normalizers[$i] = $container->register('.debug.serializer.normalizer.'.$normalizer, TraceableNormalizer::class)
->setArguments([$normalizer, new Reference('serializer.data_collector')]);
}
foreach ($encoders as $i => $encoder) {
$encoders[$i] = $container->register('.debug.serializer.encoder.'.$encoder, TraceableEncoder::class)
->setArguments([$encoder, new Reference('serializer.data_collector')]);
}
}
$serializerDefinition = $container->getDefinition('serializer');
$serializerDefinition->replaceArgument(0, $normalizers);
$serializerDefinition->replaceArgument(1, $encoders);
}
}

View File

@@ -0,0 +1,83 @@
<?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\Encoder;
use Symfony\Component\Serializer\Exception\RuntimeException;
/**
* Decoder delegating the decoding to a chain of decoders.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @author Lukas Kahwe Smith <smith@pooteeweet.org>
*
* @final
*/
class ChainDecoder implements ContextAwareDecoderInterface
{
/**
* @var array<string, array-key>
*/
private array $decoderByFormat = [];
/**
* @param array<DecoderInterface> $decoders
*/
public function __construct(
private readonly array $decoders = []
) {
}
final public function decode(string $data, string $format, array $context = []): mixed
{
return $this->getDecoder($format, $context)->decode($data, $format, $context);
}
public function supportsDecoding(string $format, array $context = []): bool
{
try {
$this->getDecoder($format, $context);
} catch (RuntimeException) {
return false;
}
return true;
}
/**
* Gets the decoder supporting the format.
*
* @throws RuntimeException if no decoder is found
*/
private function getDecoder(string $format, array $context): DecoderInterface
{
if (isset($this->decoderByFormat[$format])
&& isset($this->decoders[$this->decoderByFormat[$format]])
) {
return $this->decoders[$this->decoderByFormat[$format]];
}
$cache = true;
foreach ($this->decoders as $i => $decoder) {
$cache = $cache && !$decoder instanceof ContextAwareDecoderInterface;
if ($decoder->supportsDecoding($format, $context)) {
if ($cache) {
$this->decoderByFormat[$format] = $i;
}
return $decoder;
}
}
throw new RuntimeException(sprintf('No decoder found for format "%s".', $format));
}
}

View File

@@ -0,0 +1,106 @@
<?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\Encoder;
use Symfony\Component\Serializer\Debug\TraceableEncoder;
use Symfony\Component\Serializer\Exception\RuntimeException;
/**
* Encoder delegating the decoding to a chain of encoders.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @author Lukas Kahwe Smith <smith@pooteeweet.org>
*
* @final
*/
class ChainEncoder implements ContextAwareEncoderInterface
{
/**
* @var array<string, array-key>
*/
private array $encoderByFormat = [];
/**
* @param array<EncoderInterface> $encoders
*/
public function __construct(
private readonly array $encoders = []
) {
}
final public function encode(mixed $data, string $format, array $context = []): string
{
return $this->getEncoder($format, $context)->encode($data, $format, $context);
}
public function supportsEncoding(string $format, array $context = []): bool
{
try {
$this->getEncoder($format, $context);
} catch (RuntimeException) {
return false;
}
return true;
}
/**
* Checks whether the normalization is needed for the given format.
*/
public function needsNormalization(string $format, array $context = []): bool
{
$encoder = $this->getEncoder($format, $context);
if ($encoder instanceof TraceableEncoder) {
return $encoder->needsNormalization();
}
if (!$encoder instanceof NormalizationAwareInterface) {
return true;
}
if ($encoder instanceof self) {
return $encoder->needsNormalization($format, $context);
}
return false;
}
/**
* Gets the encoder supporting the format.
*
* @throws RuntimeException if no encoder is found
*/
private function getEncoder(string $format, array $context): EncoderInterface
{
if (isset($this->encoderByFormat[$format])
&& isset($this->encoders[$this->encoderByFormat[$format]])
) {
return $this->encoders[$this->encoderByFormat[$format]];
}
$cache = true;
foreach ($this->encoders as $i => $encoder) {
$cache = $cache && !$encoder instanceof ContextAwareEncoderInterface;
if ($encoder->supportsEncoding($format, $context)) {
if ($cache) {
$this->encoderByFormat[$format] = $i;
}
return $encoder;
}
}
throw new RuntimeException(sprintf('No encoder found for format "%s".', $format));
}
}

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\Encoder;
/**
* Adds the support of an extra $context parameter for the supportsDecoding method.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
interface ContextAwareDecoderInterface extends DecoderInterface
{
/**
* @param array $context options that decoders have access to
*/
public function supportsDecoding(string $format, array $context = []): bool;
}

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\Encoder;
/**
* Adds the support of an extra $context parameter for the supportsEncoding method.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
interface ContextAwareEncoderInterface extends EncoderInterface
{
/**
* @param array $context options that encoders have access to
*/
public function supportsEncoding(string $format, array $context = []): bool;
}

View File

@@ -0,0 +1,285 @@
<?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\Encoder;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
/**
* Encodes CSV data.
*
* @author Kévin Dunglas <dunglas@gmail.com>
* @author Oliver Hoff <oliver@hofff.com>
*/
class CsvEncoder implements EncoderInterface, DecoderInterface
{
public const FORMAT = 'csv';
public const DELIMITER_KEY = 'csv_delimiter';
public const ENCLOSURE_KEY = 'csv_enclosure';
public const ESCAPE_CHAR_KEY = 'csv_escape_char';
public const KEY_SEPARATOR_KEY = 'csv_key_separator';
public const HEADERS_KEY = 'csv_headers';
public const ESCAPE_FORMULAS_KEY = 'csv_escape_formulas';
public const AS_COLLECTION_KEY = 'as_collection';
public const NO_HEADERS_KEY = 'no_headers';
public const END_OF_LINE = 'csv_end_of_line';
public const OUTPUT_UTF8_BOM_KEY = 'output_utf8_bom';
private const UTF8_BOM = "\xEF\xBB\xBF";
private const FORMULAS_START_CHARACTERS = ['=', '-', '+', '@', "\t", "\r"];
private array $defaultContext = [
self::DELIMITER_KEY => ',',
self::ENCLOSURE_KEY => '"',
self::ESCAPE_CHAR_KEY => '',
self::END_OF_LINE => "\n",
self::ESCAPE_FORMULAS_KEY => false,
self::HEADERS_KEY => [],
self::KEY_SEPARATOR_KEY => '.',
self::NO_HEADERS_KEY => false,
self::AS_COLLECTION_KEY => true,
self::OUTPUT_UTF8_BOM_KEY => false,
];
public function __construct(array $defaultContext = [])
{
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
}
public function encode(mixed $data, string $format, array $context = []): string
{
$handle = fopen('php://temp,', 'w+');
if (!is_iterable($data)) {
$data = [[$data]];
} elseif (empty($data)) {
$data = [[]];
} else {
// Sequential arrays of arrays are considered as collections
$i = 0;
foreach ($data as $key => $value) {
if ($i !== $key || !\is_array($value)) {
$data = [$data];
break;
}
++$i;
}
}
[$delimiter, $enclosure, $escapeChar, $keySeparator, $headers, $escapeFormulas, $outputBom] = $this->getCsvOptions($context);
foreach ($data as &$value) {
$flattened = [];
$this->flatten($value, $flattened, $keySeparator, '', $escapeFormulas);
$value = $flattened;
}
unset($value);
$headers = array_merge(array_values($headers), array_diff($this->extractHeaders($data), $headers));
$endOfLine = $context[self::END_OF_LINE] ?? $this->defaultContext[self::END_OF_LINE];
if (!($context[self::NO_HEADERS_KEY] ?? $this->defaultContext[self::NO_HEADERS_KEY])) {
fputcsv($handle, $headers, $delimiter, $enclosure, $escapeChar);
if ("\n" !== $endOfLine && 0 === fseek($handle, -1, \SEEK_CUR)) {
fwrite($handle, $endOfLine);
}
}
$headers = array_fill_keys($headers, '');
foreach ($data as $row) {
fputcsv($handle, array_replace($headers, $row), $delimiter, $enclosure, $escapeChar);
if ("\n" !== $endOfLine && 0 === fseek($handle, -1, \SEEK_CUR)) {
fwrite($handle, $endOfLine);
}
}
rewind($handle);
$value = stream_get_contents($handle);
fclose($handle);
if ($outputBom) {
if (!preg_match('//u', $value)) {
throw new UnexpectedValueException('You are trying to add a UTF-8 BOM to a non UTF-8 text.');
}
$value = self::UTF8_BOM.$value;
}
return $value;
}
public function supportsEncoding(string $format): bool
{
return self::FORMAT === $format;
}
public function decode(string $data, string $format, array $context = []): mixed
{
$handle = fopen('php://temp', 'r+');
fwrite($handle, $data);
rewind($handle);
if (str_starts_with($data, self::UTF8_BOM)) {
fseek($handle, \strlen(self::UTF8_BOM));
}
$headers = null;
$nbHeaders = 0;
$headerCount = [];
$result = [];
[$delimiter, $enclosure, $escapeChar, $keySeparator, , , , $asCollection] = $this->getCsvOptions($context);
while (false !== ($cols = fgetcsv($handle, 0, $delimiter, $enclosure, $escapeChar))) {
$nbCols = \count($cols);
if (null === $headers) {
$nbHeaders = $nbCols;
if ($context[self::NO_HEADERS_KEY] ?? $this->defaultContext[self::NO_HEADERS_KEY]) {
for ($i = 0; $i < $nbCols; ++$i) {
$headers[] = [$i];
}
$headerCount = array_fill(0, $nbCols, 1);
} else {
foreach ($cols as $col) {
$header = explode($keySeparator, $col ?? '');
$headers[] = $header;
$headerCount[] = \count($header);
}
continue;
}
}
$item = [];
for ($i = 0; ($i < $nbCols) && ($i < $nbHeaders); ++$i) {
$depth = $headerCount[$i];
$arr = &$item;
for ($j = 0; $j < $depth; ++$j) {
$headerName = $headers[$i][$j];
if ('' === $headerName) {
$headerName = $i;
}
// Handle nested arrays
if ($j === ($depth - 1)) {
$arr[$headerName] = $cols[$i];
continue;
}
if (!isset($arr[$headerName])) {
$arr[$headerName] = [];
}
$arr = &$arr[$headerName];
}
}
$result[] = $item;
}
fclose($handle);
if ($asCollection) {
return $result;
}
if (empty($result) || isset($result[1])) {
return $result;
}
// If there is only one data line in the document, return it (the line), the result is not considered as a collection
return $result[0];
}
public function supportsDecoding(string $format): bool
{
return self::FORMAT === $format;
}
/**
* Flattens an array and generates keys including the path.
*/
private function flatten(iterable $array, array &$result, string $keySeparator, string $parentKey = '', bool $escapeFormulas = false): void
{
foreach ($array as $key => $value) {
if (is_iterable($value)) {
$this->flatten($value, $result, $keySeparator, $parentKey.$key.$keySeparator, $escapeFormulas);
} else {
if ($escapeFormulas && \in_array(substr((string) $value, 0, 1), self::FORMULAS_START_CHARACTERS, true)) {
$result[$parentKey.$key] = "'".$value;
} else {
// Ensures an actual value is used when dealing with true and false
$result[$parentKey.$key] = false === $value ? 0 : (true === $value ? 1 : $value);
}
}
}
}
private function getCsvOptions(array $context): array
{
$delimiter = $context[self::DELIMITER_KEY] ?? $this->defaultContext[self::DELIMITER_KEY];
$enclosure = $context[self::ENCLOSURE_KEY] ?? $this->defaultContext[self::ENCLOSURE_KEY];
$escapeChar = $context[self::ESCAPE_CHAR_KEY] ?? $this->defaultContext[self::ESCAPE_CHAR_KEY];
$keySeparator = $context[self::KEY_SEPARATOR_KEY] ?? $this->defaultContext[self::KEY_SEPARATOR_KEY];
$headers = $context[self::HEADERS_KEY] ?? $this->defaultContext[self::HEADERS_KEY];
$escapeFormulas = $context[self::ESCAPE_FORMULAS_KEY] ?? $this->defaultContext[self::ESCAPE_FORMULAS_KEY];
$outputBom = $context[self::OUTPUT_UTF8_BOM_KEY] ?? $this->defaultContext[self::OUTPUT_UTF8_BOM_KEY];
$asCollection = $context[self::AS_COLLECTION_KEY] ?? $this->defaultContext[self::AS_COLLECTION_KEY];
if (!\is_array($headers)) {
throw new InvalidArgumentException(sprintf('The "%s" context variable must be an array or null, given "%s".', self::HEADERS_KEY, get_debug_type($headers)));
}
return [$delimiter, $enclosure, $escapeChar, $keySeparator, $headers, $escapeFormulas, $outputBom, $asCollection];
}
/**
* @return string[]
*/
private function extractHeaders(iterable $data): array
{
$headers = [];
$flippedHeaders = [];
foreach ($data as $row) {
$previousHeader = null;
foreach ($row as $header => $_) {
if (isset($flippedHeaders[$header])) {
$previousHeader = $header;
continue;
}
if (null === $previousHeader) {
$n = \count($headers);
} else {
$n = $flippedHeaders[$previousHeader] + 1;
for ($j = \count($headers); $j > $n; --$j) {
++$flippedHeaders[$headers[$j] = $headers[$j - 1]];
}
}
$headers[$n] = $header;
$flippedHeaders[$header] = $n;
$previousHeader = $header;
}
}
return $headers;
}
}

View File

@@ -0,0 +1,47 @@
<?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\Encoder;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
interface DecoderInterface
{
/**
* Decodes a string into PHP data.
*
* @param string $data Data to decode
* @param string $format Format name
* @param array $context Options that decoders have access to
*
* The format parameter specifies which format the data is in; valid values
* depend on the specific implementation. Authors implementing this interface
* are encouraged to document which formats they support in a non-inherited
* phpdoc comment.
*
* @return mixed
*
* @throws UnexpectedValueException
*/
public function decode(string $data, string $format, array $context = []);
/**
* Checks whether the deserializer can decode from given format.
*
* @param string $format Format name
*
* @return bool
*/
public function supportsDecoding(string $format);
}

View File

@@ -0,0 +1,38 @@
<?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\Encoder;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
interface EncoderInterface
{
/**
* Encodes data into the given format.
*
* @param mixed $data Data to encode
* @param string $format Format name
* @param array $context Options that normalizers/encoders have access to
*
* @throws UnexpectedValueException
*/
public function encode(mixed $data, string $format, array $context = []): string;
/**
* Checks whether the serializer can encode to given format.
*
* @param string $format Format name
*/
public function supportsEncoding(string $format): bool;
}

View File

@@ -0,0 +1,124 @@
<?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\Encoder;
use Seld\JsonLint\JsonParser;
use Symfony\Component\Serializer\Exception\NotEncodableValueException;
use Symfony\Component\Serializer\Exception\UnsupportedException;
/**
* Decodes JSON data.
*
* @author Sander Coolen <sander@jibber.nl>
*/
class JsonDecode implements DecoderInterface
{
/**
* @deprecated since Symfony 6.4, to be removed in 7.0
*/
protected $serializer;
/**
* True to return the result as an associative array, false for a nested stdClass hierarchy.
*/
public const ASSOCIATIVE = 'json_decode_associative';
/**
* True to enable seld/jsonlint as a source for more specific error messages when json_decode fails.
*/
public const DETAILED_ERROR_MESSAGES = 'json_decode_detailed_errors';
public const OPTIONS = 'json_decode_options';
/**
* Specifies the recursion depth.
*/
public const RECURSION_DEPTH = 'json_decode_recursion_depth';
private array $defaultContext = [
self::ASSOCIATIVE => false,
self::DETAILED_ERROR_MESSAGES => false,
self::OPTIONS => 0,
self::RECURSION_DEPTH => 512,
];
public function __construct(array $defaultContext = [])
{
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
}
/**
* Decodes data.
*
* @param string $data The encoded JSON string to decode
* @param string $format Must be set to JsonEncoder::FORMAT
* @param array $context An optional set of options for the JSON decoder; see below
*
* The $context array is a simple key=>value array, with the following supported keys:
*
* json_decode_associative: boolean
* If true, returns the object as an associative array.
* If false, returns the object as nested stdClass
* If not specified, this method will use the default set in JsonDecode::__construct
*
* json_decode_recursion_depth: integer
* Specifies the maximum recursion depth
* If not specified, this method will use the default set in JsonDecode::__construct
*
* json_decode_options: integer
* Specifies additional options as per documentation for json_decode
*
* json_decode_detailed_errors: bool
* If true, enables seld/jsonlint as a source for more specific error messages when json_decode fails.
* If false or not specified, this method will use default error messages from PHP's json_decode
*
* @throws NotEncodableValueException
*
* @see https://php.net/json_decode
*/
public function decode(string $data, string $format, array $context = []): mixed
{
$associative = $context[self::ASSOCIATIVE] ?? $this->defaultContext[self::ASSOCIATIVE];
$recursionDepth = $context[self::RECURSION_DEPTH] ?? $this->defaultContext[self::RECURSION_DEPTH];
$options = $context[self::OPTIONS] ?? $this->defaultContext[self::OPTIONS];
try {
$decodedData = json_decode($data, $associative, $recursionDepth, $options);
} catch (\JsonException $e) {
throw new NotEncodableValueException($e->getMessage(), 0, $e);
}
if (\JSON_THROW_ON_ERROR & $options) {
return $decodedData;
}
if (\JSON_ERROR_NONE === json_last_error()) {
return $decodedData;
}
$errorMessage = json_last_error_msg();
if (!($context[self::DETAILED_ERROR_MESSAGES] ?? $this->defaultContext[self::DETAILED_ERROR_MESSAGES])) {
throw new NotEncodableValueException($errorMessage);
}
if (!class_exists(JsonParser::class)) {
throw new UnsupportedException(sprintf('Enabling "%s" serializer option requires seld/jsonlint. Try running "composer require seld/jsonlint".', self::DETAILED_ERROR_MESSAGES));
}
throw new NotEncodableValueException((new JsonParser())->lint($data)?->getMessage() ?: $errorMessage);
}
public function supportsDecoding(string $format): bool
{
return JsonEncoder::FORMAT === $format;
}
}

View File

@@ -0,0 +1,62 @@
<?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\Encoder;
use Symfony\Component\Serializer\Exception\NotEncodableValueException;
/**
* Encodes JSON data.
*
* @author Sander Coolen <sander@jibber.nl>
*/
class JsonEncode implements EncoderInterface
{
/**
* Configure the JSON flags bitmask.
*/
public const OPTIONS = 'json_encode_options';
private array $defaultContext = [
self::OPTIONS => \JSON_PRESERVE_ZERO_FRACTION,
];
public function __construct(array $defaultContext = [])
{
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
}
public function encode(mixed $data, string $format, array $context = []): string
{
$options = $context[self::OPTIONS] ?? $this->defaultContext[self::OPTIONS];
try {
$encodedJson = json_encode($data, $options);
} catch (\JsonException $e) {
throw new NotEncodableValueException($e->getMessage(), 0, $e);
}
if (\JSON_THROW_ON_ERROR & $options) {
return $encodedJson;
}
if (\JSON_ERROR_NONE !== json_last_error() && (false === $encodedJson || !($options & \JSON_PARTIAL_OUTPUT_ON_ERROR))) {
throw new NotEncodableValueException(json_last_error_msg());
}
return $encodedJson;
}
public function supportsEncoding(string $format): bool
{
return JsonEncoder::FORMAT === $format;
}
}

View File

@@ -0,0 +1,60 @@
<?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\Encoder;
/**
* Encodes JSON data.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class JsonEncoder implements EncoderInterface, DecoderInterface
{
public const FORMAT = 'json';
protected $encodingImpl;
protected $decodingImpl;
private array $defaultContext = [
JsonDecode::ASSOCIATIVE => true,
];
public function __construct(?JsonEncode $encodingImpl = null, ?JsonDecode $decodingImpl = null, array $defaultContext = [])
{
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
$this->encodingImpl = $encodingImpl ?? new JsonEncode($this->defaultContext);
$this->decodingImpl = $decodingImpl ?? new JsonDecode($this->defaultContext);
}
public function encode(mixed $data, string $format, array $context = []): string
{
$context = array_merge($this->defaultContext, $context);
return $this->encodingImpl->encode($data, self::FORMAT, $context);
}
public function decode(string $data, string $format, array $context = []): mixed
{
$context = array_merge($this->defaultContext, $context);
return $this->decodingImpl->decode($data, self::FORMAT, $context);
}
public function supportsEncoding(string $format): bool
{
return self::FORMAT === $format;
}
public function supportsDecoding(string $format): bool
{
return self::FORMAT === $format;
}
}

View File

@@ -0,0 +1,24 @@
<?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\Encoder;
/**
* Defines the interface of encoders that will normalize data themselves.
*
* Implementing this interface essentially just tells the Serializer that the
* data should not be pre-normalized before being passed to this Encoder.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
interface NormalizationAwareInterface
{
}

View File

@@ -0,0 +1,505 @@
<?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\Encoder;
use Symfony\Component\Serializer\Exception\BadMethodCallException;
use Symfony\Component\Serializer\Exception\NotEncodableValueException;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerAwareTrait;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
* @author John Wards <jwards@whiteoctober.co.uk>
* @author Fabian Vogler <fabian@equivalence.ch>
* @author Kévin Dunglas <dunglas@gmail.com>
* @author Dany Maillard <danymaillard93b@gmail.com>
*/
class XmlEncoder implements EncoderInterface, DecoderInterface, NormalizationAwareInterface, SerializerAwareInterface
{
use SerializerAwareTrait;
public const FORMAT = 'xml';
public const AS_COLLECTION = 'as_collection';
/**
* An array of ignored XML node types while decoding, each one of the DOM Predefined XML_* constants.
*/
public const DECODER_IGNORED_NODE_TYPES = 'decoder_ignored_node_types';
/**
* An array of ignored XML node types while encoding, each one of the DOM Predefined XML_* constants.
*/
public const ENCODER_IGNORED_NODE_TYPES = 'encoder_ignored_node_types';
public const ENCODING = 'xml_encoding';
public const FORMAT_OUTPUT = 'xml_format_output';
/**
* A bit field of LIBXML_* constants for loading XML documents.
*/
public const LOAD_OPTIONS = 'load_options';
/**
* A bit field of LIBXML_* constants for saving XML documents.
*/
public const SAVE_OPTIONS = 'save_options';
public const REMOVE_EMPTY_TAGS = 'remove_empty_tags';
public const ROOT_NODE_NAME = 'xml_root_node_name';
public const STANDALONE = 'xml_standalone';
public const TYPE_CAST_ATTRIBUTES = 'xml_type_cast_attributes';
public const VERSION = 'xml_version';
public const CDATA_WRAPPING = 'cdata_wrapping';
private array $defaultContext = [
self::AS_COLLECTION => false,
self::DECODER_IGNORED_NODE_TYPES => [\XML_PI_NODE, \XML_COMMENT_NODE],
self::ENCODER_IGNORED_NODE_TYPES => [],
self::LOAD_OPTIONS => \LIBXML_NONET | \LIBXML_NOBLANKS,
self::SAVE_OPTIONS => 0,
self::REMOVE_EMPTY_TAGS => false,
self::ROOT_NODE_NAME => 'response',
self::TYPE_CAST_ATTRIBUTES => true,
self::CDATA_WRAPPING => true,
];
public function __construct(array $defaultContext = [])
{
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
}
public function encode(mixed $data, string $format, array $context = []): string
{
$encoderIgnoredNodeTypes = $context[self::ENCODER_IGNORED_NODE_TYPES] ?? $this->defaultContext[self::ENCODER_IGNORED_NODE_TYPES];
$ignorePiNode = \in_array(\XML_PI_NODE, $encoderIgnoredNodeTypes, true);
if ($data instanceof \DOMDocument) {
return $data->saveXML($ignorePiNode ? $data->documentElement : null);
}
$xmlRootNodeName = $context[self::ROOT_NODE_NAME] ?? $this->defaultContext[self::ROOT_NODE_NAME];
$dom = $this->createDomDocument($context);
if (null !== $data && !\is_scalar($data)) {
$root = $dom->createElement($xmlRootNodeName);
$dom->appendChild($root);
$this->buildXml($root, $data, $format, $context, $xmlRootNodeName);
} else {
$this->appendNode($dom, $data, $format, $context, $xmlRootNodeName);
}
return $dom->saveXML($ignorePiNode ? $dom->documentElement : null, $context[self::SAVE_OPTIONS] ?? $this->defaultContext[self::SAVE_OPTIONS]);
}
public function decode(string $data, string $format, array $context = []): mixed
{
if ('' === trim($data)) {
throw new NotEncodableValueException('Invalid XML data, it cannot be empty.');
}
$internalErrors = libxml_use_internal_errors(true);
libxml_clear_errors();
$dom = new \DOMDocument();
$dom->loadXML($data, $context[self::LOAD_OPTIONS] ?? $this->defaultContext[self::LOAD_OPTIONS]);
libxml_use_internal_errors($internalErrors);
if ($error = libxml_get_last_error()) {
libxml_clear_errors();
throw new NotEncodableValueException($error->message);
}
$rootNode = null;
$decoderIgnoredNodeTypes = $context[self::DECODER_IGNORED_NODE_TYPES] ?? $this->defaultContext[self::DECODER_IGNORED_NODE_TYPES];
foreach ($dom->childNodes as $child) {
if (\in_array($child->nodeType, $decoderIgnoredNodeTypes, true)) {
continue;
}
if (\XML_DOCUMENT_TYPE_NODE === $child->nodeType) {
throw new NotEncodableValueException('Document types are not allowed.');
}
if (!$rootNode) {
$rootNode = $child;
}
}
// todo: throw an exception if the root node name is not correctly configured (bc)
if ($rootNode->hasChildNodes()) {
$data = $this->parseXml($rootNode, $context);
if (\is_array($data)) {
$data = $this->addXmlNamespaces($data, $rootNode, $dom);
}
return $data;
}
if (!$rootNode->hasAttributes()) {
return $rootNode->nodeValue;
}
$data = array_merge($this->parseXmlAttributes($rootNode, $context), ['#' => $rootNode->nodeValue]);
$data = $this->addXmlNamespaces($data, $rootNode, $dom);
return $data;
}
public function supportsEncoding(string $format): bool
{
return self::FORMAT === $format;
}
public function supportsDecoding(string $format): bool
{
return self::FORMAT === $format;
}
final protected function appendXMLString(\DOMNode $node, string $val): bool
{
if ('' !== $val) {
$frag = $node->ownerDocument->createDocumentFragment();
$frag->appendXML($val);
$node->appendChild($frag);
return true;
}
return false;
}
final protected function appendText(\DOMNode $node, string $val): bool
{
$nodeText = $node->ownerDocument->createTextNode($val);
$node->appendChild($nodeText);
return true;
}
final protected function appendCData(\DOMNode $node, string $val): bool
{
$nodeText = $node->ownerDocument->createCDATASection($val);
$node->appendChild($nodeText);
return true;
}
final protected function appendDocumentFragment(\DOMNode $node, \DOMDocumentFragment $fragment): bool
{
if ($fragment instanceof \DOMDocumentFragment) {
$node->appendChild($fragment);
return true;
}
return false;
}
final protected function appendComment(\DOMNode $node, string $data): bool
{
$node->appendChild($node->ownerDocument->createComment($data));
return true;
}
/**
* Checks the name is a valid xml element name.
*/
final protected function isElementNameValid(string $name): bool
{
return $name
&& !str_contains($name, ' ')
&& preg_match('#^[\pL_][\pL0-9._:-]*$#ui', $name);
}
/**
* Parse the input DOMNode into an array or a string.
*/
private function parseXml(\DOMNode $node, array $context = []): array|string
{
$data = $this->parseXmlAttributes($node, $context);
$value = $this->parseXmlValue($node, $context);
if (!\count($data)) {
return $value;
}
if (!\is_array($value)) {
$data['#'] = $value;
return $data;
}
if (1 === \count($value) && key($value)) {
$data[key($value)] = current($value);
return $data;
}
foreach ($value as $key => $val) {
$data[$key] = $val;
}
return $data;
}
/**
* Parse the input DOMNode attributes into an array.
*/
private function parseXmlAttributes(\DOMNode $node, array $context = []): array
{
if (!$node->hasAttributes()) {
return [];
}
$data = [];
$typeCastAttributes = (bool) ($context[self::TYPE_CAST_ATTRIBUTES] ?? $this->defaultContext[self::TYPE_CAST_ATTRIBUTES]);
foreach ($node->attributes as $attr) {
if (!is_numeric($attr->nodeValue) || !$typeCastAttributes || (isset($attr->nodeValue[1]) && '0' === $attr->nodeValue[0] && '.' !== $attr->nodeValue[1])) {
$data['@'.$attr->nodeName] = $attr->nodeValue;
continue;
}
if (false !== $val = filter_var($attr->nodeValue, \FILTER_VALIDATE_INT)) {
$data['@'.$attr->nodeName] = $val;
continue;
}
$data['@'.$attr->nodeName] = (float) $attr->nodeValue;
}
return $data;
}
/**
* Parse the input DOMNode value (content and children) into an array or a string.
*/
private function parseXmlValue(\DOMNode $node, array $context = []): array|string
{
if (!$node->hasChildNodes()) {
return $node->nodeValue;
}
if (1 === $node->childNodes->length && \in_array($node->firstChild->nodeType, [\XML_TEXT_NODE, \XML_CDATA_SECTION_NODE])) {
return $node->firstChild->nodeValue;
}
$value = [];
$decoderIgnoredNodeTypes = $context[self::DECODER_IGNORED_NODE_TYPES] ?? $this->defaultContext[self::DECODER_IGNORED_NODE_TYPES];
foreach ($node->childNodes as $subnode) {
if (\in_array($subnode->nodeType, $decoderIgnoredNodeTypes, true)) {
continue;
}
$val = $this->parseXml($subnode, $context);
if ('item' === $subnode->nodeName && isset($val['@key'])) {
$value[$val['@key']] = $val['#'] ?? $val;
} else {
$value[$subnode->nodeName][] = $val;
}
}
$asCollection = $context[self::AS_COLLECTION] ?? $this->defaultContext[self::AS_COLLECTION];
foreach ($value as $key => $val) {
if (!$asCollection && \is_array($val) && 1 === \count($val)) {
$value[$key] = current($val);
}
}
return $value;
}
private function addXmlNamespaces(array $data, \DOMNode $node, \DOMDocument $document): array
{
$xpath = new \DOMXPath($document);
foreach ($xpath->query('namespace::*', $node) as $nsNode) {
$data['@'.$nsNode->nodeName] = $nsNode->nodeValue;
}
unset($data['@xmlns:xml']);
return $data;
}
/**
* Parse the data and convert it to DOMElements.
*
* @throws NotEncodableValueException
*/
private function buildXml(\DOMNode $parentNode, mixed $data, string $format, array $context, ?string $xmlRootNodeName = null): bool
{
$append = true;
$removeEmptyTags = $context[self::REMOVE_EMPTY_TAGS] ?? $this->defaultContext[self::REMOVE_EMPTY_TAGS] ?? false;
$encoderIgnoredNodeTypes = $context[self::ENCODER_IGNORED_NODE_TYPES] ?? $this->defaultContext[self::ENCODER_IGNORED_NODE_TYPES];
if (\is_array($data) || ($data instanceof \Traversable && (null === $this->serializer || !$this->serializer->supportsNormalization($data, $format)))) {
foreach ($data as $key => $data) {
// Ah this is the magic @ attribute types.
if (str_starts_with($key, '@') && $this->isElementNameValid($attributeName = substr($key, 1))) {
if (!\is_scalar($data)) {
$data = $this->serializer->normalize($data, $format, $context);
}
if (\is_bool($data)) {
$data = (int) $data;
}
$parentNode->setAttribute($attributeName, $data);
} elseif ('#' === $key) {
$append = $this->selectNodeType($parentNode, $data, $format, $context);
} elseif ('#comment' === $key) {
if (!\in_array(\XML_COMMENT_NODE, $encoderIgnoredNodeTypes, true)) {
$append = $this->appendComment($parentNode, $data);
}
} elseif (\is_array($data) && false === is_numeric($key)) {
// Is this array fully numeric keys?
if (ctype_digit(implode('', array_keys($data)))) {
/*
* Create nodes to append to $parentNode based on the $key of this array
* Produces <xml><item>0</item><item>1</item></xml>
* From ["item" => [0,1]];.
*/
foreach ($data as $subData) {
$append = $this->appendNode($parentNode, $subData, $format, $context, $key);
}
} else {
$append = $this->appendNode($parentNode, $data, $format, $context, $key);
}
} elseif (is_numeric($key) || !$this->isElementNameValid($key)) {
$append = $this->appendNode($parentNode, $data, $format, $context, 'item', $key);
} elseif (null !== $data || !$removeEmptyTags) {
$append = $this->appendNode($parentNode, $data, $format, $context, $key);
}
}
return $append;
}
if (\is_object($data)) {
if (null === $this->serializer) {
throw new BadMethodCallException(sprintf('The serializer needs to be set to allow "%s()" to be used with object data.', __METHOD__));
}
$data = $this->serializer->normalize($data, $format, $context);
if (null !== $data && !\is_scalar($data)) {
return $this->buildXml($parentNode, $data, $format, $context, $xmlRootNodeName);
}
// top level data object was normalized into a scalar
if (!$parentNode->parentNode->parentNode) {
$root = $parentNode->parentNode;
$root->removeChild($parentNode);
return $this->appendNode($root, $data, $format, $context, $xmlRootNodeName);
}
return $this->appendNode($parentNode, $data, $format, $context, 'data');
}
throw new NotEncodableValueException('An unexpected value could not be serialized: '.(!\is_resource($data) ? var_export($data, true) : sprintf('%s resource', get_resource_type($data))));
}
/**
* Selects the type of node to create and appends it to the parent.
*/
private function appendNode(\DOMNode $parentNode, mixed $data, string $format, array $context, string $nodeName, ?string $key = null): bool
{
$dom = $parentNode instanceof \DOMDocument ? $parentNode : $parentNode->ownerDocument;
$node = $dom->createElement($nodeName);
if (null !== $key) {
$node->setAttribute('key', $key);
}
$appendNode = $this->selectNodeType($node, $data, $format, $context);
// we may have decided not to append this node, either in error or if its $nodeName is not valid
if ($appendNode) {
$parentNode->appendChild($node);
}
return $appendNode;
}
/**
* Checks if a value contains any characters which would require CDATA wrapping.
*/
private function needsCdataWrapping(string $val, array $context): bool
{
return ($context[self::CDATA_WRAPPING] ?? $this->defaultContext[self::CDATA_WRAPPING]) && preg_match('/[<>&]/', $val);
}
/**
* Tests the value being passed and decide what sort of element to create.
*
* @throws NotEncodableValueException
*/
private function selectNodeType(\DOMNode $node, mixed $val, string $format, array $context): bool
{
if (\is_array($val)) {
return $this->buildXml($node, $val, $format, $context);
} elseif ($val instanceof \SimpleXMLElement) {
$child = $node->ownerDocument->importNode(dom_import_simplexml($val), true);
$node->appendChild($child);
} elseif ($val instanceof \Traversable) {
$this->buildXml($node, $val, $format, $context);
} elseif ($val instanceof \DOMNode) {
$child = $node->ownerDocument->importNode($val, true);
$node->appendChild($child);
} elseif (\is_object($val)) {
if (null === $this->serializer) {
throw new BadMethodCallException(sprintf('The serializer needs to be set to allow "%s()" to be used with object data.', __METHOD__));
}
return $this->selectNodeType($node, $this->serializer->normalize($val, $format, $context), $format, $context);
} elseif (is_numeric($val)) {
return $this->appendText($node, (string) $val);
} elseif (\is_string($val) && $this->needsCdataWrapping($val, $context)) {
return $this->appendCData($node, $val);
} elseif (\is_string($val)) {
return $this->appendText($node, $val);
} elseif (\is_bool($val)) {
return $this->appendText($node, (int) $val);
}
return true;
}
/**
* Create a DOM document, taking serializer options into account.
*/
private function createDomDocument(array $context): \DOMDocument
{
$document = new \DOMDocument();
// Set an attribute on the DOM document specifying, as part of the XML declaration,
$xmlOptions = [
// nicely formats output with indentation and extra space
self::FORMAT_OUTPUT => 'formatOutput',
// the version number of the document
self::VERSION => 'xmlVersion',
// the encoding of the document
self::ENCODING => 'encoding',
// whether the document is standalone
self::STANDALONE => 'xmlStandalone',
];
foreach ($xmlOptions as $xmlOption => $documentProperty) {
if ($contextOption = $context[$xmlOption] ?? $this->defaultContext[$xmlOption] ?? false) {
$document->$documentProperty = $contextOption;
}
}
return $document;
}
}

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\Encoder;
use Symfony\Component\Serializer\Exception\RuntimeException;
use Symfony\Component\Yaml\Dumper;
use Symfony\Component\Yaml\Parser;
use Symfony\Component\Yaml\Yaml;
/**
* Encodes YAML data.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class YamlEncoder implements EncoderInterface, DecoderInterface
{
public const FORMAT = 'yaml';
private const ALTERNATIVE_FORMAT = 'yml';
public const PRESERVE_EMPTY_OBJECTS = 'preserve_empty_objects';
/**
* Override the amount of spaces to use for indentation of nested nodes.
*
* This option only works in the constructor, not in calls to `encode`.
*/
public const YAML_INDENTATION = 'yaml_indentation';
public const YAML_INLINE = 'yaml_inline';
/**
* Initial indentation for root element.
*/
public const YAML_INDENT = 'yaml_indent';
public const YAML_FLAGS = 'yaml_flags';
private readonly Dumper $dumper;
private readonly Parser $parser;
private array $defaultContext = [
self::YAML_INLINE => 0,
self::YAML_INDENT => 0,
self::YAML_FLAGS => 0,
];
public function __construct(?Dumper $dumper = null, ?Parser $parser = null, array $defaultContext = [])
{
if (!class_exists(Dumper::class)) {
throw new RuntimeException('The YamlEncoder class requires the "Yaml" component. Try running "composer require symfony/yaml".');
}
if (!$dumper) {
$dumper = \array_key_exists(self::YAML_INDENTATION, $defaultContext) ? new Dumper($defaultContext[self::YAML_INDENTATION]) : new Dumper();
}
$this->dumper = $dumper;
$this->parser = $parser ?? new Parser();
unset($defaultContext[self::YAML_INDENTATION]);
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
}
public function encode(mixed $data, string $format, array $context = []): string
{
$context = array_merge($this->defaultContext, $context);
if ($context[self::PRESERVE_EMPTY_OBJECTS] ?? false) {
$context[self::YAML_FLAGS] |= Yaml::DUMP_OBJECT_AS_MAP;
}
return $this->dumper->dump($data, $context[self::YAML_INLINE], $context[self::YAML_INDENT], $context[self::YAML_FLAGS]);
}
public function supportsEncoding(string $format): bool
{
return self::FORMAT === $format || self::ALTERNATIVE_FORMAT === $format;
}
public function decode(string $data, string $format, array $context = []): mixed
{
$context = array_merge($this->defaultContext, $context);
return $this->parser->parse($data, $context[self::YAML_FLAGS]);
}
public function supportsDecoding(string $format): bool
{
return self::FORMAT === $format || self::ALTERNATIVE_FORMAT === $format;
}
}

View File

@@ -0,0 +1,16 @@
<?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\Exception;
class BadMethodCallException extends \BadMethodCallException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?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\Exception;
/**
* CircularReferenceException.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class CircularReferenceException extends RuntimeException
{
}

View File

@@ -0,0 +1,21 @@
<?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\Exception;
/**
* Base exception interface.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
interface ExceptionInterface extends \Throwable
{
}

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\Exception;
/**
* ExtraAttributesException.
*
* @author Julien DIDIER <julien@didier.io>
*/
class ExtraAttributesException extends RuntimeException
{
public function __construct(
private readonly array $extraAttributes,
?\Throwable $previous = null,
) {
$msg = sprintf('Extra attributes are not allowed ("%s" %s unknown).', implode('", "', $extraAttributes), \count($extraAttributes) > 1 ? 'are' : 'is');
parent::__construct($msg, 0, $previous);
}
/**
* Get the extra attributes that are not allowed.
*/
public function getExtraAttributes(): array
{
return $this->extraAttributes;
}
}

View File

@@ -0,0 +1,21 @@
<?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\Exception;
/**
* InvalidArgumentException.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?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\Exception;
/**
* LogicException.
*
* @author Lukas Kahwe Smith <smith@pooteeweet.org>
*/
class LogicException extends \LogicException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?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\Exception;
/**
* MappingException.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class MappingException extends RuntimeException
{
}

View File

@@ -0,0 +1,48 @@
<?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\Exception;
/**
* @author Maxime VEBER <maxime.veber@nekland.fr>
*/
class MissingConstructorArgumentsException extends RuntimeException
{
/**
* @param string[] $missingArguments
* @param class-string|null $class
*/
public function __construct(
string $message,
int $code = 0,
?\Throwable $previous = null,
private array $missingArguments = [],
private ?string $class = null,
) {
parent::__construct($message, $code, $previous);
}
/**
* @return string[]
*/
public function getMissingConstructorArguments(): array
{
return $this->missingArguments;
}
/**
* @return class-string|null
*/
public function getClass(): ?string
{
return $this->class;
}
}

View File

@@ -0,0 +1,19 @@
<?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\Exception;
/**
* @author Christian Flothmann <christian.flothmann@sensiolabs.de>
*/
class NotEncodableValueException extends UnexpectedValueException
{
}

View File

@@ -0,0 +1,64 @@
<?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\Exception;
/**
* @author Christian Flothmann <christian.flothmann@sensiolabs.de>
*/
class NotNormalizableValueException extends UnexpectedValueException
{
private ?string $currentType = null;
private ?array $expectedTypes = null;
private ?string $path = null;
private bool $useMessageForUser = false;
/**
* @param string[] $expectedTypes
* @param bool $useMessageForUser If the message passed to this exception is something that can be shown
* safely to your user. In other words, avoid catching other exceptions and
* passing their message directly to this class.
*/
public static function createForUnexpectedDataType(string $message, mixed $data, array $expectedTypes, ?string $path = null, bool $useMessageForUser = false, int $code = 0, ?\Throwable $previous = null): self
{
$self = new self($message, $code, $previous);
$self->currentType = get_debug_type($data);
$self->expectedTypes = $expectedTypes;
$self->path = $path;
$self->useMessageForUser = $useMessageForUser;
return $self;
}
public function getCurrentType(): ?string
{
return $this->currentType;
}
/**
* @return string[]|null
*/
public function getExpectedTypes(): ?array
{
return $this->expectedTypes;
}
public function getPath(): ?string
{
return $this->path;
}
public function canUseMessageForUser(): ?bool
{
return $this->useMessageForUser;
}
}

View File

@@ -0,0 +1,43 @@
<?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\Exception;
/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class PartialDenormalizationException extends UnexpectedValueException
{
/**
* @param NotNormalizableValueException[] $errors
*/
public function __construct(
private mixed $data,
private array $errors,
) {
}
/**
* @return mixed
*/
public function getData()
{
return $this->data;
}
/**
* @return NotNormalizableValueException[]
*/
public function getErrors(): array
{
return $this->errors;
}
}

View File

@@ -0,0 +1,21 @@
<?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\Exception;
/**
* RuntimeException.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?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\Exception;
/**
* UnexpectedValueException.
*
* @author Lukas Kahwe Smith <smith@pooteeweet.org>
*/
class UnexpectedValueException extends \UnexpectedValueException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?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\Exception;
/**
* UnsupportedException.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class UnsupportedException extends InvalidArgumentException
{
}

View File

@@ -0,0 +1,19 @@
<?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\Exception;
/**
* @author Konstantin Myakshin <molodchick@gmail.com>
*/
class UnsupportedFormatException extends NotEncodableValueException
{
}

View File

@@ -0,0 +1,36 @@
<?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\Extractor;
use Symfony\Component\PropertyInfo\PropertyListExtractorInterface;
/**
* @author David Maicher <mail@dmaicher.de>
*/
final class ObjectPropertyListExtractor implements ObjectPropertyListExtractorInterface
{
private PropertyListExtractorInterface $propertyListExtractor;
private \Closure $objectClassResolver;
public function __construct(PropertyListExtractorInterface $propertyListExtractor, ?callable $objectClassResolver = null)
{
$this->propertyListExtractor = $propertyListExtractor;
$this->objectClassResolver = ($objectClassResolver ?? 'get_class')(...);
}
public function getProperties(object $object, array $context = []): ?array
{
$class = ($this->objectClassResolver)($object);
return $this->propertyListExtractor->getProperties($class, $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\Extractor;
/**
* @author David Maicher <mail@dmaicher.de>
*/
interface ObjectPropertyListExtractorInterface
{
/**
* Gets the list of properties available for the given object.
*
* @return string[]|null
*/
public function getProperties(object $object, array $context = []): ?array;
}

19
vendor/symfony/serializer/LICENSE vendored Executable file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2004-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,230 @@
<?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\Mapping;
use Symfony\Component\PropertyAccess\PropertyPath;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class AttributeMetadata implements AttributeMetadataInterface
{
/**
* @internal This property is public in order to reduce the size of the
* class' serialized representation. Do not access it. Use
* {@link getName()} instead.
*/
public string $name;
/**
* @internal This property is public in order to reduce the size of the
* class' serialized representation. Do not access it. Use
* {@link getGroups()} instead.
*/
public array $groups = [];
/**
* @internal This property is public in order to reduce the size of the
* class' serialized representation. Do not access it. Use
* {@link getMaxDepth()} instead.
*/
public ?int $maxDepth = null;
/**
* @internal This property is public in order to reduce the size of the
* class' serialized representation. Do not access it. Use
* {@link getSerializedName()} instead.
*/
public ?string $serializedName = null;
/**
* @internal This property is public in order to reduce the size of the
* class' serialized representation. Do not access it. Use
* {@link getSerializedPath()} instead.
*/
public ?PropertyPath $serializedPath = null;
/**
* @internal This property is public in order to reduce the size of the
* class' serialized representation. Do not access it. Use
* {@link isIgnored()} instead.
*/
public bool $ignore = false;
/**
* @var array[] Normalization contexts per group name ("*" applies to all groups)
*
* @internal This property is public in order to reduce the size of the
* class' serialized representation. Do not access it. Use
* {@link getNormalizationContexts()} instead.
*/
public array $normalizationContexts = [];
/**
* @var array[] Denormalization contexts per group name ("*" applies to all groups)
*
* @internal This property is public in order to reduce the size of the
* class' serialized representation. Do not access it. Use
* {@link getDenormalizationContexts()} instead.
*/
public array $denormalizationContexts = [];
public function __construct(string $name)
{
$this->name = $name;
}
public function getName(): string
{
return $this->name;
}
public function addGroup(string $group): void
{
if (!\in_array($group, $this->groups)) {
$this->groups[] = $group;
}
}
public function getGroups(): array
{
return $this->groups;
}
public function setMaxDepth(?int $maxDepth): void
{
$this->maxDepth = $maxDepth;
}
public function getMaxDepth(): ?int
{
return $this->maxDepth;
}
public function setSerializedName(?string $serializedName = null): void
{
if (1 > \func_num_args()) {
trigger_deprecation('symfony/serializer', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
}
$this->serializedName = $serializedName;
}
public function getSerializedName(): ?string
{
return $this->serializedName;
}
public function setSerializedPath(?PropertyPath $serializedPath = null): void
{
$this->serializedPath = $serializedPath;
}
public function getSerializedPath(): ?PropertyPath
{
return $this->serializedPath;
}
public function setIgnore(bool $ignore): void
{
$this->ignore = $ignore;
}
public function isIgnored(): bool
{
return $this->ignore;
}
public function getNormalizationContexts(): array
{
return $this->normalizationContexts;
}
public function getNormalizationContextForGroups(array $groups): array
{
$contexts = [];
foreach ($groups as $group) {
$contexts[] = $this->normalizationContexts[$group] ?? [];
}
return array_merge($this->normalizationContexts['*'] ?? [], ...$contexts);
}
public function setNormalizationContextForGroups(array $context, array $groups = []): void
{
if (!$groups) {
$this->normalizationContexts['*'] = $context;
}
foreach ($groups as $group) {
$this->normalizationContexts[$group] = $context;
}
}
public function getDenormalizationContexts(): array
{
return $this->denormalizationContexts;
}
public function getDenormalizationContextForGroups(array $groups): array
{
$contexts = [];
foreach ($groups as $group) {
$contexts[] = $this->denormalizationContexts[$group] ?? [];
}
return array_merge($this->denormalizationContexts['*'] ?? [], ...$contexts);
}
public function setDenormalizationContextForGroups(array $context, array $groups = []): void
{
if (!$groups) {
$this->denormalizationContexts['*'] = $context;
}
foreach ($groups as $group) {
$this->denormalizationContexts[$group] = $context;
}
}
public function merge(AttributeMetadataInterface $attributeMetadata): void
{
foreach ($attributeMetadata->getGroups() as $group) {
$this->addGroup($group);
}
// Overwrite only if not defined
$this->maxDepth ??= $attributeMetadata->getMaxDepth();
$this->serializedName ??= $attributeMetadata->getSerializedName();
$this->serializedPath ??= $attributeMetadata->getSerializedPath();
// Overwrite only if both contexts are empty
if (!$this->normalizationContexts && !$this->denormalizationContexts) {
$this->normalizationContexts = $attributeMetadata->getNormalizationContexts();
$this->denormalizationContexts = $attributeMetadata->getDenormalizationContexts();
}
if ($ignore = $attributeMetadata->isIgnored()) {
$this->ignore = $ignore;
}
}
/**
* Returns the names of the properties that should be serialized.
*
* @return string[]
*/
public function __sleep(): array
{
return ['name', 'groups', 'maxDepth', 'serializedName', 'serializedPath', 'ignore', 'normalizationContexts', 'denormalizationContexts'];
}
}

View File

@@ -0,0 +1,112 @@
<?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\Mapping;
use Symfony\Component\PropertyAccess\PropertyPath;
/**
* Stores metadata needed for serializing and deserializing attributes.
*
* Primarily, the metadata stores serialization groups.
*
* @internal
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
interface AttributeMetadataInterface
{
/**
* Gets the attribute name.
*/
public function getName(): string;
/**
* Adds this attribute to the given group.
*/
public function addGroup(string $group): void;
/**
* Gets groups of this attribute.
*
* @return string[]
*/
public function getGroups(): array;
/**
* Sets the serialization max depth for this attribute.
*/
public function setMaxDepth(?int $maxDepth): void;
/**
* Gets the serialization max depth for this attribute.
*/
public function getMaxDepth(): ?int;
/**
* Sets the serialization name for this attribute.
*/
public function setSerializedName(?string $serializedName): void;
/**
* Gets the serialization name for this attribute.
*/
public function getSerializedName(): ?string;
public function setSerializedPath(?PropertyPath $serializedPath): void;
public function getSerializedPath(): ?PropertyPath;
/**
* Sets if this attribute must be ignored or not.
*/
public function setIgnore(bool $ignore): void;
/**
* Gets if this attribute is ignored or not.
*/
public function isIgnored(): bool;
/**
* Merges an {@see AttributeMetadataInterface} with in the current one.
*/
public function merge(self $attributeMetadata): void;
/**
* Gets all the normalization contexts per group ("*" being the base context applied to all groups).
*/
public function getNormalizationContexts(): array;
/**
* Gets the computed normalization contexts for given groups.
*/
public function getNormalizationContextForGroups(array $groups): array;
/**
* Sets the normalization context for given groups.
*/
public function setNormalizationContextForGroups(array $context, array $groups = []): void;
/**
* Gets all the denormalization contexts per group ("*" being the base context applied to all groups).
*/
public function getDenormalizationContexts(): array;
/**
* Gets the computed denormalization contexts for given groups.
*/
public function getDenormalizationContextForGroups(array $groups): array;
/**
* Sets the denormalization context for given groups.
*/
public function setDenormalizationContextForGroups(array $context, array $groups = []): void;
}

View File

@@ -0,0 +1,81 @@
<?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\Mapping;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
/**
* @author Samuel Roze <samuel.roze@gmail.com>
*/
class ClassDiscriminatorFromClassMetadata implements ClassDiscriminatorResolverInterface
{
private array $mappingForMappedObjectCache = [];
public function __construct(
private readonly ClassMetadataFactoryInterface $classMetadataFactory,
) {
}
public function getMappingForClass(string $class): ?ClassDiscriminatorMapping
{
if ($this->classMetadataFactory->hasMetadataFor($class)) {
return $this->classMetadataFactory->getMetadataFor($class)->getClassDiscriminatorMapping();
}
return null;
}
public function getMappingForMappedObject(object|string $object): ?ClassDiscriminatorMapping
{
if ($this->classMetadataFactory->hasMetadataFor($object)) {
$metadata = $this->classMetadataFactory->getMetadataFor($object);
if (null !== $metadata->getClassDiscriminatorMapping()) {
return $metadata->getClassDiscriminatorMapping();
}
}
$cacheKey = \is_object($object) ? $object::class : $object;
if (!\array_key_exists($cacheKey, $this->mappingForMappedObjectCache)) {
$this->mappingForMappedObjectCache[$cacheKey] = $this->resolveMappingForMappedObject($object);
}
return $this->mappingForMappedObjectCache[$cacheKey];
}
public function getTypeForMappedObject(object|string $object): ?string
{
if (null === $mapping = $this->getMappingForMappedObject($object)) {
return null;
}
return $mapping->getMappedObjectType($object);
}
private function resolveMappingForMappedObject(object|string $object): ?ClassDiscriminatorMapping
{
$reflectionClass = new \ReflectionClass($object);
if ($parentClass = $reflectionClass->getParentClass()) {
if (null !== ($parentMapping = $this->getMappingForMappedObject($parentClass->getName()))) {
return $parentMapping;
}
}
foreach ($reflectionClass->getInterfaceNames() as $interfaceName) {
if (null !== ($interfaceMapping = $this->getMappingForMappedObject($interfaceName))) {
return $interfaceMapping;
}
}
return null;
}
}

View File

@@ -0,0 +1,64 @@
<?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\Mapping;
/**
* @author Samuel Roze <samuel.roze@gmail.com>
*/
class ClassDiscriminatorMapping
{
/**
* @param array<string, string> $typesMapping
*/
public function __construct(
private readonly string $typeProperty,
private array $typesMapping = [],
) {
uasort($this->typesMapping, static function (string $a, string $b): int {
if (is_a($a, $b, true)) {
return -1;
}
if (is_a($b, $a, true)) {
return 1;
}
return 0;
});
}
public function getTypeProperty(): string
{
return $this->typeProperty;
}
public function getClassForType(string $type): ?string
{
return $this->typesMapping[$type] ?? null;
}
public function getMappedObjectType(object|string $object): ?string
{
foreach ($this->typesMapping as $type => $typeClass) {
if (is_a($object, $typeClass, true)) {
return $type;
}
}
return null;
}
public function getTypesMapping(): array
{
return $this->typesMapping;
}
}

View File

@@ -0,0 +1,26 @@
<?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\Mapping;
/**
* Knows how to get the class discriminator mapping for classes and objects.
*
* @author Samuel Roze <samuel.roze@gmail.com>
*/
interface ClassDiscriminatorResolverInterface
{
public function getMappingForClass(string $class): ?ClassDiscriminatorMapping;
public function getMappingForMappedObject(object|string $object): ?ClassDiscriminatorMapping;
public function getTypeForMappedObject(object|string $object): ?string;
}

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\Mapping;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class ClassMetadata implements ClassMetadataInterface
{
/**
* @internal This property is public in order to reduce the size of the
* class' serialized representation. Do not access it. Use
* {@link getName()} instead.
*/
public string $name;
/**
* @var AttributeMetadataInterface[]
*
* @internal This property is public in order to reduce the size of the
* class' serialized representation. Do not access it. Use
* {@link getAttributesMetadata()} instead.
*/
public array $attributesMetadata = [];
private ?\ReflectionClass $reflClass = null;
/**
* @internal This property is public in order to reduce the size of the
* class' serialized representation. Do not access it. Use
* {@link getClassDiscriminatorMapping()} instead.
*/
public ?ClassDiscriminatorMapping $classDiscriminatorMapping = null;
/**
* Constructs a metadata for the given class.
*/
public function __construct(string $class, ?ClassDiscriminatorMapping $classDiscriminatorMapping = null)
{
$this->name = $class;
$this->classDiscriminatorMapping = $classDiscriminatorMapping;
}
public function getName(): string
{
return $this->name;
}
public function addAttributeMetadata(AttributeMetadataInterface $attributeMetadata): void
{
$this->attributesMetadata[$attributeMetadata->getName()] = $attributeMetadata;
}
public function getAttributesMetadata(): array
{
return $this->attributesMetadata;
}
public function merge(ClassMetadataInterface $classMetadata): void
{
foreach ($classMetadata->getAttributesMetadata() as $attributeMetadata) {
if (isset($this->attributesMetadata[$attributeMetadata->getName()])) {
$this->attributesMetadata[$attributeMetadata->getName()]->merge($attributeMetadata);
} else {
$this->addAttributeMetadata($attributeMetadata);
}
}
}
public function getReflectionClass(): \ReflectionClass
{
if (!$this->reflClass) {
$this->reflClass = new \ReflectionClass($this->getName());
}
return $this->reflClass;
}
public function getClassDiscriminatorMapping(): ?ClassDiscriminatorMapping
{
return $this->classDiscriminatorMapping;
}
public function setClassDiscriminatorMapping(?ClassDiscriminatorMapping $mapping = null): void
{
if (1 > \func_num_args()) {
trigger_deprecation('symfony/serializer', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
}
$this->classDiscriminatorMapping = $mapping;
}
/**
* Returns the names of the properties that should be serialized.
*
* @return string[]
*/
public function __sleep(): array
{
return [
'name',
'attributesMetadata',
'classDiscriminatorMapping',
];
}
}

View File

@@ -0,0 +1,57 @@
<?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\Mapping;
/**
* Stores metadata needed for serializing and deserializing objects of specific class.
*
* Primarily, the metadata stores the set of attributes to serialize or deserialize.
*
* There may only exist one metadata for each attribute according to its name.
*
* @internal
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
interface ClassMetadataInterface
{
/**
* Returns the name of the backing PHP class.
*/
public function getName(): string;
/**
* Adds an {@link AttributeMetadataInterface}.
*/
public function addAttributeMetadata(AttributeMetadataInterface $attributeMetadata): void;
/**
* Gets the list of {@link AttributeMetadataInterface}.
*
* @return array<string, AttributeMetadataInterface>
*/
public function getAttributesMetadata(): array;
/**
* Merges a {@link ClassMetadataInterface} in the current one.
*/
public function merge(self $classMetadata): void;
/**
* Returns a {@link \ReflectionClass} instance for this class.
*/
public function getReflectionClass(): \ReflectionClass;
public function getClassDiscriminatorMapping(): ?ClassDiscriminatorMapping;
public function setClassDiscriminatorMapping(?ClassDiscriminatorMapping $mapping): void;
}

View File

@@ -0,0 +1,62 @@
<?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\Mapping\Factory;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Serializer\Mapping\ClassMetadataInterface;
/**
* Caches metadata using a PSR-6 implementation.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class CacheClassMetadataFactory implements ClassMetadataFactoryInterface
{
use ClassResolverTrait;
/**
* @var array<string, ClassMetadataInterface>
*/
private array $loadedClasses = [];
public function __construct(
private readonly ClassMetadataFactoryInterface $decorated,
private readonly CacheItemPoolInterface $cacheItemPool,
) {
}
public function getMetadataFor(string|object $value): ClassMetadataInterface
{
$class = $this->getClass($value);
if (isset($this->loadedClasses[$class])) {
return $this->loadedClasses[$class];
}
$key = rawurlencode(strtr($class, '\\', '_'));
$item = $this->cacheItemPool->getItem($key);
if ($item->isHit()) {
return $this->loadedClasses[$class] = $item->get();
}
$metadata = $this->decorated->getMetadataFor($value);
$this->cacheItemPool->save($item->set($metadata));
return $this->loadedClasses[$class] = $metadata;
}
public function hasMetadataFor(mixed $value): bool
{
return $this->decorated->hasMetadataFor($value);
}
}

View File

@@ -0,0 +1,67 @@
<?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\Mapping\Factory;
use Symfony\Component\Serializer\Mapping\ClassMetadata;
use Symfony\Component\Serializer\Mapping\ClassMetadataInterface;
use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface;
/**
* Returns a {@link ClassMetadata}.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class ClassMetadataFactory implements ClassMetadataFactoryInterface
{
use ClassResolverTrait;
/**
* @var array<string, ClassMetadataInterface>
*/
private array $loadedClasses;
public function __construct(
private readonly LoaderInterface $loader,
) {
}
public function getMetadataFor(string|object $value): ClassMetadataInterface
{
$class = $this->getClass($value);
if (isset($this->loadedClasses[$class])) {
return $this->loadedClasses[$class];
}
$classMetadata = new ClassMetadata($class);
$this->loader->loadClassMetadata($classMetadata);
$reflectionClass = $classMetadata->getReflectionClass();
// Include metadata from the parent class
if ($parent = $reflectionClass->getParentClass()) {
$classMetadata->merge($this->getMetadataFor($parent->name));
}
// Include metadata from all implemented interfaces
foreach ($reflectionClass->getInterfaces() as $interface) {
$classMetadata->merge($this->getMetadataFor($interface->name));
}
return $this->loadedClasses[$class] = $classMetadata;
}
public function hasMetadataFor(mixed $value): bool
{
return \is_object($value) || (\is_string($value) && (class_exists($value) || interface_exists($value, false)));
}
}

View File

@@ -0,0 +1,68 @@
<?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\Mapping\Factory;
use Symfony\Component\Serializer\Mapping\ClassMetadataInterface;
use Symfony\Component\VarExporter\VarExporter;
/**
* @author Fabien Bourigault <bourigaultfabien@gmail.com>
*/
final class ClassMetadataFactoryCompiler
{
/**
* @param ClassMetadataInterface[] $classMetadatas
*/
public function compile(array $classMetadatas): string
{
return <<<EOF
<?php
// This file has been auto-generated by the Symfony Serializer Component.
return [{$this->generateDeclaredClassMetadata($classMetadatas)}
];
EOF;
}
/**
* @param ClassMetadataInterface[] $classMetadatas
*/
private function generateDeclaredClassMetadata(array $classMetadatas): string
{
$compiled = '';
foreach ($classMetadatas as $classMetadata) {
$attributesMetadata = [];
foreach ($classMetadata->getAttributesMetadata() as $attributeMetadata) {
$attributesMetadata[$attributeMetadata->getName()] = [
$attributeMetadata->getGroups(),
$attributeMetadata->getMaxDepth(),
$attributeMetadata->getSerializedName(),
$attributeMetadata->getSerializedPath(),
];
}
$classDiscriminatorMapping = $classMetadata->getClassDiscriminatorMapping() ? [
$classMetadata->getClassDiscriminatorMapping()->getTypeProperty(),
$classMetadata->getClassDiscriminatorMapping()->getTypesMapping(),
] : null;
$compiled .= sprintf("\n'%s' => %s,", $classMetadata->getName(), VarExporter::export([
$attributesMetadata,
$classDiscriminatorMapping,
]));
}
return $compiled;
}
}

View File

@@ -0,0 +1,45 @@
<?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\Mapping\Factory;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Mapping\ClassMetadataInterface;
/**
* Returns a {@see ClassMetadataInterface}.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
interface ClassMetadataFactoryInterface
{
/**
* If the method was called with the same class name (or an object of that
* class) before, the same metadata instance is returned.
*
* If the factory was configured with a cache, this method will first look
* for an existing metadata instance in the cache. If an existing instance
* is found, it will be returned without further ado.
*
* Otherwise, a new metadata instance is created. If the factory was
* configured with a loader, the metadata is passed to the
* {@link \Symfony\Component\Serializer\Mapping\Loader\LoaderInterface::loadClassMetadata()} method for further
* configuration. At last, the new object is returned.
*
* @throws InvalidArgumentException
*/
public function getMetadataFor(string|object $value): ClassMetadataInterface;
/**
* Checks if class has metadata.
*/
public function hasMetadataFor(mixed $value): bool;
}

View File

@@ -0,0 +1,42 @@
<?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\Mapping\Factory;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
/**
* Resolves a class name.
*
* @internal
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
trait ClassResolverTrait
{
/**
* Gets a class name for a given class or instance.
*
* @throws InvalidArgumentException If the class does not exist
*/
private function getClass(object|string $value): string
{
if (\is_string($value)) {
if (!class_exists($value) && !interface_exists($value, false)) {
throw new InvalidArgumentException(sprintf('The class or interface "%s" does not exist.', $value));
}
return ltrim($value, '\\');
}
return $value::class;
}
}

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\Mapping\Factory;
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping;
use Symfony\Component\Serializer\Mapping\ClassMetadata;
use Symfony\Component\Serializer\Mapping\ClassMetadataInterface;
/**
* @author Fabien Bourigault <bourigaultfabien@gmail.com>
*/
final class CompiledClassMetadataFactory implements ClassMetadataFactoryInterface
{
private array $compiledClassMetadata = [];
private array $loadedClasses = [];
public function __construct(
string $compiledClassMetadataFile,
private readonly ClassMetadataFactoryInterface $classMetadataFactory,
) {
if (!file_exists($compiledClassMetadataFile)) {
throw new \RuntimeException("File \"{$compiledClassMetadataFile}\" could not be found.");
}
$compiledClassMetadata = require $compiledClassMetadataFile;
if (!\is_array($compiledClassMetadata)) {
throw new \RuntimeException(sprintf('Compiled metadata must be of the type array, %s given.', \gettype($compiledClassMetadata)));
}
$this->compiledClassMetadata = $compiledClassMetadata;
}
public function getMetadataFor(string|object $value): ClassMetadataInterface
{
$className = \is_object($value) ? $value::class : $value;
if (!isset($this->compiledClassMetadata[$className])) {
return $this->classMetadataFactory->getMetadataFor($value);
}
if (!isset($this->loadedClasses[$className])) {
$classMetadata = new ClassMetadata($className);
foreach ($this->compiledClassMetadata[$className][0] as $name => $compiledAttributesMetadata) {
$classMetadata->attributesMetadata[$name] = $attributeMetadata = new AttributeMetadata($name);
[$attributeMetadata->groups, $attributeMetadata->maxDepth, $attributeMetadata->serializedName] = $compiledAttributesMetadata;
}
$classMetadata->classDiscriminatorMapping = $this->compiledClassMetadata[$className][1]
? new ClassDiscriminatorMapping(...$this->compiledClassMetadata[$className][1])
: null
;
$this->loadedClasses[$className] = $classMetadata;
}
return $this->loadedClasses[$className];
}
public function hasMetadataFor(mixed $value): bool
{
$className = \is_object($value) ? $value::class : $value;
return isset($this->compiledClassMetadata[$className]) || $this->classMetadataFactory->hasMetadataFor($value);
}
}

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\Mapping\Loader;
trigger_deprecation('symfony/serializer', '6.4', 'The "%s" class is deprecated, use "%s" instead.', AnnotationLoader::class, AttributeLoader::class);
class_exists(AttributeLoader::class);
if (false) {
/**
* @deprecated since Symfony 6.4, to be removed in 7.0, use {@link AttributeLoader} instead
*/
class AnnotationLoader extends AttributeLoader
{
}
}

View File

@@ -0,0 +1,311 @@
<?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\Mapping\Loader;
use Doctrine\Common\Annotations\Reader;
use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Attribute\DiscriminatorMap;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Serializer\Attribute\Ignore;
use Symfony\Component\Serializer\Attribute\MaxDepth;
use Symfony\Component\Serializer\Attribute\SerializedName;
use Symfony\Component\Serializer\Attribute\SerializedPath;
use Symfony\Component\Serializer\Exception\MappingException;
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping;
use Symfony\Component\Serializer\Mapping\ClassMetadataInterface;
/**
* Loader for PHP attributes.
*
* @author Kévin Dunglas <dunglas@gmail.com>
* @author Alexander M. Turek <me@derrabus.de>
* @author Alexandre Daubois <alex.daubois@gmail.com>
*/
class AttributeLoader implements LoaderInterface
{
private const KNOWN_ATTRIBUTES = [
DiscriminatorMap::class,
Groups::class,
Ignore::class,
MaxDepth::class,
SerializedName::class,
SerializedPath::class,
Context::class,
];
public function __construct(
private readonly ?Reader $reader = null,
) {
if ($reader) {
trigger_deprecation('symfony/serializer', '6.4', 'Passing a "%s" instance as argument 1 to "%s()" is deprecated, pass null or omit the parameter instead.', get_debug_type($reader), __METHOD__);
}
}
public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool
{
$reflectionClass = $classMetadata->getReflectionClass();
$className = $reflectionClass->name;
$loaded = false;
$classGroups = [];
$classContextAnnotation = null;
$attributesMetadata = $classMetadata->getAttributesMetadata();
foreach ($this->loadAttributes($reflectionClass) as $annotation) {
if ($annotation instanceof DiscriminatorMap) {
$classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping(
$annotation->getTypeProperty(),
$annotation->getMapping()
));
continue;
}
if ($annotation instanceof Groups) {
$classGroups = $annotation->getGroups();
continue;
}
if ($annotation instanceof Context) {
$classContextAnnotation = $annotation;
}
}
foreach ($reflectionClass->getProperties() as $property) {
if (!isset($attributesMetadata[$property->name])) {
$attributesMetadata[$property->name] = new AttributeMetadata($property->name);
$classMetadata->addAttributeMetadata($attributesMetadata[$property->name]);
}
if ($property->getDeclaringClass()->name === $className) {
if ($classContextAnnotation) {
$this->setAttributeContextsForGroups($classContextAnnotation, $attributesMetadata[$property->name]);
}
foreach ($classGroups as $group) {
$attributesMetadata[$property->name]->addGroup($group);
}
foreach ($this->loadAttributes($property) as $annotation) {
if ($annotation instanceof Groups) {
foreach ($annotation->getGroups() as $group) {
$attributesMetadata[$property->name]->addGroup($group);
}
} elseif ($annotation instanceof MaxDepth) {
$attributesMetadata[$property->name]->setMaxDepth($annotation->getMaxDepth());
} elseif ($annotation instanceof SerializedName) {
$attributesMetadata[$property->name]->setSerializedName($annotation->getSerializedName());
} elseif ($annotation instanceof SerializedPath) {
$attributesMetadata[$property->name]->setSerializedPath($annotation->getSerializedPath());
} elseif ($annotation instanceof Ignore) {
$attributesMetadata[$property->name]->setIgnore(true);
} elseif ($annotation instanceof Context) {
$this->setAttributeContextsForGroups($annotation, $attributesMetadata[$property->name]);
}
$loaded = true;
}
}
}
foreach ($reflectionClass->getMethods() as $method) {
if ($method->getDeclaringClass()->name !== $className) {
continue;
}
if (0 === stripos($method->name, 'get') && $method->getNumberOfRequiredParameters()) {
continue; /* matches the BC behavior in `Symfony\Component\Serializer\Normalizer\ObjectNormalizer::extractAttributes` */
}
$accessorOrMutator = preg_match('/^(get|is|has|set)(.+)$/i', $method->name, $matches);
if ($accessorOrMutator) {
$attributeName = lcfirst($matches[2]);
if (isset($attributesMetadata[$attributeName])) {
$attributeMetadata = $attributesMetadata[$attributeName];
} else {
$attributesMetadata[$attributeName] = $attributeMetadata = new AttributeMetadata($attributeName);
$classMetadata->addAttributeMetadata($attributeMetadata);
}
}
foreach ($this->loadAttributes($method) as $annotation) {
if ($annotation instanceof Groups) {
if (!$accessorOrMutator) {
throw new MappingException(sprintf('Groups on "%s::%s()" cannot be added. Groups can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name));
}
foreach ($annotation->getGroups() as $group) {
$attributeMetadata->addGroup($group);
}
} elseif ($annotation instanceof MaxDepth) {
if (!$accessorOrMutator) {
throw new MappingException(sprintf('MaxDepth on "%s::%s()" cannot be added. MaxDepth can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name));
}
$attributeMetadata->setMaxDepth($annotation->getMaxDepth());
} elseif ($annotation instanceof SerializedName) {
if (!$accessorOrMutator) {
throw new MappingException(sprintf('SerializedName on "%s::%s()" cannot be added. SerializedName can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name));
}
$attributeMetadata->setSerializedName($annotation->getSerializedName());
} elseif ($annotation instanceof SerializedPath) {
if (!$accessorOrMutator) {
throw new MappingException(sprintf('SerializedPath on "%s::%s()" cannot be added. SerializedPath can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name));
}
$attributeMetadata->setSerializedPath($annotation->getSerializedPath());
} elseif ($annotation instanceof Ignore) {
if ($accessorOrMutator) {
$attributeMetadata->setIgnore(true);
}
} elseif ($annotation instanceof Context) {
if (!$accessorOrMutator) {
throw new MappingException(sprintf('Context on "%s::%s()" cannot be added. Context can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name));
}
$this->setAttributeContextsForGroups($annotation, $attributeMetadata);
}
$loaded = true;
}
}
return $loaded;
}
private function loadAttributes(\ReflectionMethod|\ReflectionClass|\ReflectionProperty $reflector): iterable
{
foreach ($reflector->getAttributes() as $attribute) {
if ($this->isKnownAttribute($attribute->getName())) {
try {
yield $attribute->newInstance();
} catch (\Error $e) {
if (\Error::class !== $e::class) {
throw $e;
}
$on = match (true) {
$reflector instanceof \ReflectionClass => ' on class '.$reflector->name,
$reflector instanceof \ReflectionMethod => sprintf(' on "%s::%s()"', $reflector->getDeclaringClass()->name, $reflector->name),
$reflector instanceof \ReflectionProperty => sprintf(' on "%s::$%s"', $reflector->getDeclaringClass()->name, $reflector->name),
default => '',
};
throw new MappingException(sprintf('Could not instantiate attribute "%s"%s.', $attribute->getName(), $on), 0, $e);
}
}
}
if (null === $this->reader) {
return;
}
if ($reflector instanceof \ReflectionClass) {
yield from $this->getClassAnnotations($reflector);
}
if ($reflector instanceof \ReflectionMethod) {
yield from $this->getMethodAnnotations($reflector);
}
if ($reflector instanceof \ReflectionProperty) {
yield from $this->getPropertyAnnotations($reflector);
}
}
/**
* @deprecated since Symfony 6.4 without replacement
*/
public function loadAnnotations(\ReflectionMethod|\ReflectionClass|\ReflectionProperty $reflector): iterable
{
trigger_deprecation('symfony/serializer', '6.4', 'Method "%s()" is deprecated without replacement.', __METHOD__);
return $this->loadAttributes($reflector);
}
private function setAttributeContextsForGroups(Context $annotation, AttributeMetadataInterface $attributeMetadata): void
{
if ($annotation->getContext()) {
$attributeMetadata->setNormalizationContextForGroups($annotation->getContext(), $annotation->getGroups());
$attributeMetadata->setDenormalizationContextForGroups($annotation->getContext(), $annotation->getGroups());
}
if ($annotation->getNormalizationContext()) {
$attributeMetadata->setNormalizationContextForGroups($annotation->getNormalizationContext(), $annotation->getGroups());
}
if ($annotation->getDenormalizationContext()) {
$attributeMetadata->setDenormalizationContextForGroups($annotation->getDenormalizationContext(), $annotation->getGroups());
}
}
private function isKnownAttribute(string $attributeName): bool
{
foreach (self::KNOWN_ATTRIBUTES as $knownAttribute) {
if (is_a($attributeName, $knownAttribute, true)) {
return true;
}
}
return false;
}
/**
* @return object[]
*/
private function getClassAnnotations(\ReflectionClass $reflector): array
{
if ($annotations = array_filter(
$this->reader->getClassAnnotations($reflector),
fn (object $annotation): bool => $this->isKnownAttribute($annotation::class),
)) {
trigger_deprecation('symfony/serializer', '6.4', 'Class "%s" uses Doctrine Annotations to configure serialization, which is deprecated. Use PHP attributes instead.', $reflector->getName());
}
return $annotations;
}
/**
* @return object[]
*/
private function getMethodAnnotations(\ReflectionMethod $reflector): array
{
if ($annotations = array_filter(
$this->reader->getMethodAnnotations($reflector),
fn (object $annotation): bool => $this->isKnownAttribute($annotation::class),
)) {
trigger_deprecation('symfony/serializer', '6.4', 'Method "%s::%s()" uses Doctrine Annotations to configure serialization, which is deprecated. Use PHP attributes instead.', $reflector->getDeclaringClass()->getName(), $reflector->getName());
}
return $annotations;
}
/**
* @return object[]
*/
private function getPropertyAnnotations(\ReflectionProperty $reflector): array
{
if ($annotations = array_filter(
$this->reader->getPropertyAnnotations($reflector),
fn (object $annotation): bool => $this->isKnownAttribute($annotation::class),
)) {
trigger_deprecation('symfony/serializer', '6.4', 'Property "%s::$%s" uses Doctrine Annotations to configure serialization, which is deprecated. Use PHP attributes instead.', $reflector->getDeclaringClass()->getName(), $reflector->getName());
}
return $annotations;
}
}
if (!class_exists(AnnotationLoader::class, false)) {
class_alias(AttributeLoader::class, AnnotationLoader::class);
}

View File

@@ -0,0 +1,42 @@
<?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\Mapping\Loader;
use Symfony\Component\Serializer\Exception\MappingException;
/**
* Base class for all file based loaders.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
abstract class FileLoader implements LoaderInterface
{
protected $file;
/**
* @param string $file The mapping file to load
*
* @throws MappingException if the mapping file does not exist or is not readable
*/
public function __construct(string $file)
{
if (!is_file($file)) {
throw new MappingException(sprintf('The mapping file "%s" does not exist.', $file));
}
if (!is_readable($file)) {
throw new MappingException(sprintf('The mapping file "%s" is not readable.', $file));
}
$this->file = $file;
}
}

View File

@@ -0,0 +1,64 @@
<?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\Mapping\Loader;
use Symfony\Component\Serializer\Exception\MappingException;
use Symfony\Component\Serializer\Mapping\ClassMetadataInterface;
/**
* Calls multiple {@link LoaderInterface} instances in a chain.
*
* This class accepts multiple instances of LoaderInterface to be passed to the
* constructor. When {@link loadClassMetadata()} is called, the same method is called
* in <em>all</em> of these loaders, regardless of whether any of them was
* successful or not.
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class LoaderChain implements LoaderInterface
{
/**
* Accepts a list of LoaderInterface instances.
*
* @param LoaderInterface[] $loaders An array of LoaderInterface instances
*
* @throws MappingException If any of the loaders does not implement LoaderInterface
*/
public function __construct(private readonly array $loaders)
{
foreach ($loaders as $loader) {
if (!$loader instanceof LoaderInterface) {
throw new MappingException(sprintf('Class "%s" is expected to implement LoaderInterface.', get_debug_type($loader)));
}
}
}
public function loadClassMetadata(ClassMetadataInterface $metadata): bool
{
$success = false;
foreach ($this->loaders as $loader) {
$success = $loader->loadClassMetadata($metadata) || $success;
}
return $success;
}
/**
* @return LoaderInterface[]
*/
public function getLoaders(): array
{
return $this->loaders;
}
}

View File

@@ -0,0 +1,24 @@
<?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\Mapping\Loader;
use Symfony\Component\Serializer\Mapping\ClassMetadataInterface;
/**
* Loads {@link ClassMetadataInterface}.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
interface LoaderInterface
{
public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool;
}

View File

@@ -0,0 +1,182 @@
<?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\Mapping\Loader;
use Symfony\Component\Config\Util\XmlUtils;
use Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException;
use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Component\Serializer\Exception\MappingException;
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping;
use Symfony\Component\Serializer\Mapping\ClassMetadataInterface;
/**
* Loads XML mapping files.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class XmlFileLoader extends FileLoader
{
/**
* An array of {@class \SimpleXMLElement} instances.
*
* @var \SimpleXMLElement[]|null
*/
private ?array $classes = null;
public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool
{
if (!$this->classes ??= $this->getClassesFromXml()) {
return false;
}
$attributesMetadata = $classMetadata->getAttributesMetadata();
if (isset($this->classes[$classMetadata->getName()])) {
$xml = $this->classes[$classMetadata->getName()];
foreach ($xml->attribute as $attribute) {
$attributeName = (string) $attribute['name'];
if (isset($attributesMetadata[$attributeName])) {
$attributeMetadata = $attributesMetadata[$attributeName];
} else {
$attributeMetadata = new AttributeMetadata($attributeName);
$classMetadata->addAttributeMetadata($attributeMetadata);
}
foreach ($attribute->group as $group) {
$attributeMetadata->addGroup((string) $group);
}
if (isset($attribute['max-depth'])) {
$attributeMetadata->setMaxDepth((int) $attribute['max-depth']);
}
if (isset($attribute['serialized-name'])) {
$attributeMetadata->setSerializedName((string) $attribute['serialized-name']);
}
if (isset($attribute['serialized-path'])) {
try {
$attributeMetadata->setSerializedPath(new PropertyPath((string) $attribute['serialized-path']));
} catch (InvalidPropertyPathException) {
throw new MappingException(sprintf('The "serialized-path" value must be a valid property path for the attribute "%s" of the class "%s".', $attributeName, $classMetadata->getName()));
}
}
if (isset($attribute['ignore'])) {
$attributeMetadata->setIgnore(XmlUtils::phpize($attribute['ignore']));
}
foreach ($attribute->context as $node) {
$groups = (array) $node->group;
$context = $this->parseContext($node->entry);
$attributeMetadata->setNormalizationContextForGroups($context, $groups);
$attributeMetadata->setDenormalizationContextForGroups($context, $groups);
}
foreach ($attribute->normalization_context as $node) {
$groups = (array) $node->group;
$context = $this->parseContext($node->entry);
$attributeMetadata->setNormalizationContextForGroups($context, $groups);
}
foreach ($attribute->denormalization_context as $node) {
$groups = (array) $node->group;
$context = $this->parseContext($node->entry);
$attributeMetadata->setDenormalizationContextForGroups($context, $groups);
}
}
if (isset($xml->{'discriminator-map'})) {
$mapping = [];
foreach ($xml->{'discriminator-map'}->mapping as $element) {
$elementAttributes = $element->attributes();
$mapping[(string) $elementAttributes->type] = (string) $elementAttributes->class;
}
$classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping(
(string) $xml->{'discriminator-map'}->attributes()->{'type-property'},
$mapping
));
}
return true;
}
return false;
}
/**
* Return the names of the classes mapped in this file.
*
* @return string[]
*/
public function getMappedClasses(): array
{
return array_keys($this->classes ??= $this->getClassesFromXml());
}
/**
* Parses an XML File.
*
* @throws MappingException
*/
private function parseFile(string $file): \SimpleXMLElement
{
try {
$dom = XmlUtils::loadFile($file, __DIR__.'/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd');
} catch (\Exception $e) {
throw new MappingException($e->getMessage(), $e->getCode(), $e);
}
return simplexml_import_dom($dom);
}
private function getClassesFromXml(): array
{
$xml = $this->parseFile($this->file);
$classes = [];
foreach ($xml->class as $class) {
$classes[(string) $class['name']] = $class;
}
return $classes;
}
private function parseContext(\SimpleXMLElement $nodes): array
{
$context = [];
foreach ($nodes as $node) {
if (\count($node) > 0) {
if (\count($node->entry) > 0) {
$value = $this->parseContext($node->entry);
} else {
$value = [];
}
} else {
$value = XmlUtils::phpize($node);
}
if (isset($node['name'])) {
$context[(string) $node['name']] = $value;
} else {
$context[] = $value;
}
}
return $context;
}
}

View File

@@ -0,0 +1,173 @@
<?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\Mapping\Loader;
use Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException;
use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Component\Serializer\Exception\MappingException;
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping;
use Symfony\Component\Serializer\Mapping\ClassMetadataInterface;
use Symfony\Component\Yaml\Parser;
use Symfony\Component\Yaml\Yaml;
/**
* YAML File Loader.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class YamlFileLoader extends FileLoader
{
private ?Parser $yamlParser = null;
/**
* An array of YAML class descriptions.
*/
private ?array $classes = null;
public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool
{
if (!$this->classes ??= $this->getClassesFromYaml()) {
return false;
}
if (!isset($this->classes[$classMetadata->getName()])) {
return false;
}
$yaml = $this->classes[$classMetadata->getName()];
if (isset($yaml['attributes']) && \is_array($yaml['attributes'])) {
$attributesMetadata = $classMetadata->getAttributesMetadata();
foreach ($yaml['attributes'] as $attribute => $data) {
if (isset($attributesMetadata[$attribute])) {
$attributeMetadata = $attributesMetadata[$attribute];
} else {
$attributeMetadata = new AttributeMetadata($attribute);
$classMetadata->addAttributeMetadata($attributeMetadata);
}
if (isset($data['groups'])) {
if (!\is_array($data['groups'])) {
throw new MappingException(sprintf('The "groups" key must be an array of strings in "%s" for the attribute "%s" of the class "%s".', $this->file, $attribute, $classMetadata->getName()));
}
foreach ($data['groups'] as $group) {
if (!\is_string($group)) {
throw new MappingException(sprintf('Group names must be strings in "%s" for the attribute "%s" of the class "%s".', $this->file, $attribute, $classMetadata->getName()));
}
$attributeMetadata->addGroup($group);
}
}
if (isset($data['max_depth'])) {
if (!\is_int($data['max_depth'])) {
throw new MappingException(sprintf('The "max_depth" value must be an integer in "%s" for the attribute "%s" of the class "%s".', $this->file, $attribute, $classMetadata->getName()));
}
$attributeMetadata->setMaxDepth($data['max_depth']);
}
if (isset($data['serialized_name'])) {
if (!\is_string($data['serialized_name']) || '' === $data['serialized_name']) {
throw new MappingException(sprintf('The "serialized_name" value must be a non-empty string in "%s" for the attribute "%s" of the class "%s".', $this->file, $attribute, $classMetadata->getName()));
}
$attributeMetadata->setSerializedName($data['serialized_name']);
}
if (isset($data['serialized_path'])) {
try {
$attributeMetadata->setSerializedPath(new PropertyPath((string) $data['serialized_path']));
} catch (InvalidPropertyPathException) {
throw new MappingException(sprintf('The "serialized_path" value must be a valid property path in "%s" for the attribute "%s" of the class "%s".', $this->file, $attribute, $classMetadata->getName()));
}
}
if (isset($data['ignore'])) {
if (!\is_bool($data['ignore'])) {
throw new MappingException(sprintf('The "ignore" value must be a boolean in "%s" for the attribute "%s" of the class "%s".', $this->file, $attribute, $classMetadata->getName()));
}
$attributeMetadata->setIgnore($data['ignore']);
}
foreach ($data['contexts'] ?? [] as $line) {
$groups = $line['groups'] ?? [];
if ($context = $line['context'] ?? false) {
$attributeMetadata->setNormalizationContextForGroups($context, $groups);
$attributeMetadata->setDenormalizationContextForGroups($context, $groups);
}
if ($context = $line['normalization_context'] ?? false) {
$attributeMetadata->setNormalizationContextForGroups($context, $groups);
}
if ($context = $line['denormalization_context'] ?? false) {
$attributeMetadata->setDenormalizationContextForGroups($context, $groups);
}
}
}
}
if (isset($yaml['discriminator_map'])) {
if (!isset($yaml['discriminator_map']['type_property'])) {
throw new MappingException(sprintf('The "type_property" key must be set for the discriminator map of the class "%s" in "%s".', $classMetadata->getName(), $this->file));
}
if (!isset($yaml['discriminator_map']['mapping'])) {
throw new MappingException(sprintf('The "mapping" key must be set for the discriminator map of the class "%s" in "%s".', $classMetadata->getName(), $this->file));
}
$classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping(
$yaml['discriminator_map']['type_property'],
$yaml['discriminator_map']['mapping']
));
}
return true;
}
/**
* Return the names of the classes mapped in this file.
*
* @return string[]
*/
public function getMappedClasses(): array
{
return array_keys($this->classes ??= $this->getClassesFromYaml());
}
private function getClassesFromYaml(): array
{
if (!stream_is_local($this->file)) {
throw new MappingException(sprintf('This is not a local file "%s".', $this->file));
}
$this->yamlParser ??= new Parser();
$classes = $this->yamlParser->parseFile($this->file, Yaml::PARSE_CONSTANT);
if (empty($classes)) {
return [];
}
if (!\is_array($classes)) {
throw new MappingException(sprintf('The file "%s" must contain a YAML array.', $this->file));
}
return $classes;
}
}

View File

@@ -0,0 +1,115 @@
<?xml version="1.0" ?>
<xsd:schema xmlns="http://symfony.com/schema/dic/serializer-mapping"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://symfony.com/schema/dic/serializer-mapping"
elementFormDefault="qualified">
<xsd:annotation>
<xsd:documentation><![CDATA[
Symfony Serializer Mapping Schema, version 1.0
Authors: Kévin Dunglas, Samuel Roze
A serializer mapping connects attributes with serialization groups.
]]></xsd:documentation>
</xsd:annotation>
<xsd:element name="serializer" type="serializer" />
<xsd:complexType name="serializer">
<xsd:annotation>
<xsd:documentation><![CDATA[
The root element of the serializer mapping definition.
]]></xsd:documentation>
</xsd:annotation>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="class" type="class" />
</xsd:choice>
</xsd:complexType>
<xsd:complexType name="class">
<xsd:annotation>
<xsd:documentation><![CDATA[
Contains serialization groups for a single class.
Nested elements may be class property and/or getter definitions.
]]></xsd:documentation>
</xsd:annotation>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="attribute" type="attribute" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="discriminator-map" type="discriminator-map" />
</xsd:choice>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
<xsd:complexType name="discriminator-map">
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="mapping" type="discriminator-map-mapping" maxOccurs="unbounded" />
</xsd:choice>
<xsd:attribute name="type-property" type="xsd:string" use="required" />
</xsd:complexType>
<xsd:complexType name="discriminator-map-mapping">
<xsd:attribute name="type" type="xsd:string" use="required" />
<xsd:attribute name="class" type="xsd:string" use="required" />
</xsd:complexType>
<xsd:complexType name="attribute">
<xsd:annotation>
<xsd:documentation><![CDATA[
Contains serialization groups and max depth for attributes. The name of the attribute should be given in the "name" option.
]]></xsd:documentation>
</xsd:annotation>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="group" type="xsd:string" maxOccurs="unbounded" />
<xsd:element name="context" type="context" maxOccurs="unbounded" />
<xsd:element name="normalization_context" type="context" maxOccurs="unbounded" />
<xsd:element name="denormalization_context" type="context" maxOccurs="unbounded" />
</xsd:choice>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="max-depth">
<xsd:simpleType>
<xsd:restriction base="xsd:integer">
<xsd:minInclusive value="0" />
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="serialized-name">
<xsd:simpleType>
<xsd:restriction base="xsd:string">
<xsd:minLength value="1" />
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="serialized-path">
<xsd:simpleType>
<xsd:restriction base="xsd:string">
<xsd:minLength value="1" />
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="ignore" type="xsd:boolean" />
</xsd:complexType>
<xsd:complexType name="context">
<xsd:choice maxOccurs="unbounded">
<xsd:element name="group" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="entry" type="context-root-entry" maxOccurs="unbounded" />
</xsd:choice>
</xsd:complexType>
<xsd:complexType name="context-root-entry" mixed="true">
<xsd:sequence minOccurs="0">
<xsd:element name="entry" type="context-entry" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:attribute type="xsd:string" name="name" use="required" />
</xsd:complexType>
<xsd:complexType name="context-entry" mixed="true">
<xsd:sequence minOccurs="0">
<xsd:element name="entry" type="context-entry" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:attribute type="xsd:string" name="name" />
</xsd:complexType>
</xsd:schema>

View File

@@ -0,0 +1,24 @@
<?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\NameConverter;
/**
* Gives access to the class, the format and the context in the property name converters.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
interface AdvancedNameConverterInterface extends NameConverterInterface
{
public function normalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string;
public function denormalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string;
}

View File

@@ -0,0 +1,54 @@
<?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\NameConverter;
/**
* CamelCase to Underscore name converter.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class CamelCaseToSnakeCaseNameConverter implements NameConverterInterface
{
/**
* @param array|null $attributes The list of attributes to rename or null for all attributes
* @param bool $lowerCamelCase Use lowerCamelCase style
*/
public function __construct(
private ?array $attributes = null,
private bool $lowerCamelCase = true,
) {
}
public function normalize(string $propertyName): string
{
if (null === $this->attributes || \in_array($propertyName, $this->attributes)) {
return strtolower(preg_replace('/[A-Z]/', '_\\0', lcfirst($propertyName)));
}
return $propertyName;
}
public function denormalize(string $propertyName): string
{
$camelCasedName = preg_replace_callback('/(^|_|\.)+(.)/', fn ($match) => ('.' === $match[1] ? '_' : '').strtoupper($match[2]), $propertyName);
if ($this->lowerCamelCase) {
$camelCasedName = lcfirst($camelCasedName);
}
if (null === $this->attributes || \in_array($camelCasedName, $this->attributes)) {
return $camelCasedName;
}
return $propertyName;
}
}

View File

@@ -0,0 +1,155 @@
<?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\NameConverter;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
/**
* @author Fabien Bourigault <bourigaultfabien@gmail.com>
*/
final class MetadataAwareNameConverter implements AdvancedNameConverterInterface
{
/**
* @var array<string, array<string, string|null>>
*/
private static array $normalizeCache = [];
/**
* @var array<string, array<string, string|null>>
*/
private static array $denormalizeCache = [];
/**
* @var array<string, array<string, string>>
*/
private static array $attributesMetadataCache = [];
public function __construct(
private readonly ClassMetadataFactoryInterface $metadataFactory,
private readonly ?NameConverterInterface $fallbackNameConverter = null,
) {
}
public function normalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string
{
if (null === $class) {
return $this->normalizeFallback($propertyName, $class, $format, $context);
}
if (!\array_key_exists($class, self::$normalizeCache) || !\array_key_exists($propertyName, self::$normalizeCache[$class])) {
self::$normalizeCache[$class][$propertyName] = $this->getCacheValueForNormalization($propertyName, $class);
}
return self::$normalizeCache[$class][$propertyName] ?? $this->normalizeFallback($propertyName, $class, $format, $context);
}
public function denormalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string
{
if (null === $class) {
return $this->denormalizeFallback($propertyName, $class, $format, $context);
}
$cacheKey = $this->getCacheKey($class, $context);
if (!\array_key_exists($cacheKey, self::$denormalizeCache) || !\array_key_exists($propertyName, self::$denormalizeCache[$cacheKey])) {
self::$denormalizeCache[$cacheKey][$propertyName] = $this->getCacheValueForDenormalization($propertyName, $class, $context);
}
return self::$denormalizeCache[$cacheKey][$propertyName] ?? $this->denormalizeFallback($propertyName, $class, $format, $context);
}
private function getCacheValueForNormalization(string $propertyName, string $class): ?string
{
if (!$this->metadataFactory->hasMetadataFor($class)) {
return null;
}
$attributesMetadata = $this->metadataFactory->getMetadataFor($class)->getAttributesMetadata();
if (!\array_key_exists($propertyName, $attributesMetadata)) {
return null;
}
if (null !== $attributesMetadata[$propertyName]->getSerializedName() && null !== $attributesMetadata[$propertyName]->getSerializedPath()) {
throw new LogicException(sprintf('Found SerializedName and SerializedPath attributes on property "%s" of class "%s".', $propertyName, $class));
}
return $attributesMetadata[$propertyName]->getSerializedName() ?? null;
}
private function normalizeFallback(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string
{
return $this->fallbackNameConverter ? $this->fallbackNameConverter->normalize($propertyName, $class, $format, $context) : $propertyName;
}
private function getCacheValueForDenormalization(string $propertyName, string $class, array $context): ?string
{
$cacheKey = $this->getCacheKey($class, $context);
if (!\array_key_exists($cacheKey, self::$attributesMetadataCache)) {
self::$attributesMetadataCache[$cacheKey] = $this->getCacheValueForAttributesMetadata($class, $context);
}
return self::$attributesMetadataCache[$cacheKey][$propertyName] ?? null;
}
private function denormalizeFallback(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string
{
return $this->fallbackNameConverter ? $this->fallbackNameConverter->denormalize($propertyName, $class, $format, $context) : $propertyName;
}
/**
* @return array<string, string>
*/
private function getCacheValueForAttributesMetadata(string $class, array $context): array
{
if (!$this->metadataFactory->hasMetadataFor($class)) {
return [];
}
$classMetadata = $this->metadataFactory->getMetadataFor($class);
$cache = [];
foreach ($classMetadata->getAttributesMetadata() as $name => $metadata) {
if (null === $metadata->getSerializedName()) {
continue;
}
if (null !== $metadata->getSerializedName() && null !== $metadata->getSerializedPath()) {
throw new LogicException(sprintf('Found SerializedName and SerializedPath attributes on property "%s" of class "%s".', $name, $class));
}
$metadataGroups = $metadata->getGroups();
$contextGroups = (array) ($context[AbstractNormalizer::GROUPS] ?? []);
if ($contextGroups && !$metadataGroups) {
continue;
}
if ($metadataGroups && !array_intersect($metadataGroups, $contextGroups) && !\in_array('*', $contextGroups, true)) {
continue;
}
$cache[$metadata->getSerializedName()] = $name;
}
return $cache;
}
private function getCacheKey(string $class, array $context): string
{
if (isset($context['cache_key'])) {
return $class.'-'.$context['cache_key'];
}
return $class.hash('xxh128', serialize($context[AbstractNormalizer::GROUPS] ?? []));
}
}

View File

@@ -0,0 +1,30 @@
<?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\NameConverter;
/**
* Defines the interface for property name converters.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
interface NameConverterInterface
{
/**
* Converts a property name to its normalized value.
*/
public function normalize(string $propertyName): string;
/**
* Converts a property name to its denormalized value.
*/
public function denormalize(string $propertyName): string;
}

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;
}
}

Some files were not shown because too many files have changed in this diff Show More