Upgrade 1-11.38

This commit is contained in:
xesmyd
2026-03-30 14:10:30 +02:00
parent f2a7e6d1fc
commit ac648ef29d
24665 changed files with 69682 additions and 2205004 deletions
-3
View File
@@ -1,3 +0,0 @@
vendor/
composer.lock
phpunit.xml
+1 -3
View File
@@ -29,7 +29,7 @@ class Groups
private $groups;
/**
* @throws InvalidArgumentException
* @param string[] $groups
*/
public function __construct(array $data)
{
@@ -48,8 +48,6 @@ class Groups
}
/**
* Gets groups.
*
* @return string[]
*/
public function getGroups()
+75
View File
@@ -1,6 +1,81 @@
CHANGELOG
=========
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
-----
@@ -31,7 +31,7 @@ class SerializerPass implements CompilerPassInterface
private $normalizerTag;
private $encoderTag;
public function __construct($serializerService = 'serializer', $normalizerTag = 'serializer.normalizer', $encoderTag = 'serializer.encoder')
public function __construct(string $serializerService = 'serializer', string $normalizerTag = 'serializer.normalizer', string $encoderTag = 'serializer.encoder')
{
$this->serializerService = $serializerService;
$this->normalizerTag = $normalizerTag;
+9 -11
View File
@@ -20,9 +20,9 @@ use Symfony\Component\Serializer\Exception\RuntimeException;
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @author Lukas Kahwe Smith <smith@pooteeweet.org>
*
* @final since version 3.3.
* @final
*/
class ChainDecoder implements DecoderInterface /*, ContextAwareDecoderInterface*/
class ChainDecoder implements ContextAwareDecoderInterface
{
protected $decoders = [];
protected $decoderByFormat = [];
@@ -43,10 +43,8 @@ class ChainDecoder implements DecoderInterface /*, ContextAwareDecoderInterface*
/**
* {@inheritdoc}
*/
public function supportsDecoding($format/*, array $context = []*/)
public function supportsDecoding($format, array $context = []): bool
{
$context = \func_num_args() > 1 ? func_get_arg(1) : [];
try {
$this->getDecoder($format, $context);
} catch (RuntimeException $e) {
@@ -59,13 +57,9 @@ class ChainDecoder implements DecoderInterface /*, ContextAwareDecoderInterface*
/**
* Gets the decoder supporting the format.
*
* @param string $format
*
* @return DecoderInterface
*
* @throws RuntimeException if no decoder is found
*/
private function getDecoder($format, array $context)
private function getDecoder(string $format, array $context): DecoderInterface
{
if (isset($this->decoderByFormat[$format])
&& isset($this->decoders[$this->decoderByFormat[$format]])
@@ -73,9 +67,13 @@ class ChainDecoder implements DecoderInterface /*, ContextAwareDecoderInterface*
return $this->decoders[$this->decoderByFormat[$format]];
}
$cache = true;
foreach ($this->decoders as $i => $decoder) {
$cache = $cache && !$decoder instanceof ContextAwareDecoderInterface;
if ($decoder->supportsDecoding($format, $context)) {
$this->decoderByFormat[$format] = $i;
if ($cache) {
$this->decoderByFormat[$format] = $i;
}
return $decoder;
}
+10 -17
View File
@@ -20,9 +20,9 @@ use Symfony\Component\Serializer\Exception\RuntimeException;
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @author Lukas Kahwe Smith <smith@pooteeweet.org>
*
* @final since version 3.3.
* @final
*/
class ChainEncoder implements EncoderInterface /*, ContextAwareEncoderInterface*/
class ChainEncoder implements ContextAwareEncoderInterface
{
protected $encoders = [];
protected $encoderByFormat = [];
@@ -43,10 +43,8 @@ class ChainEncoder implements EncoderInterface /*, ContextAwareEncoderInterface*
/**
* {@inheritdoc}
*/
public function supportsEncoding($format/*, array $context = []*/)
public function supportsEncoding($format, array $context = []): bool
{
$context = \func_num_args() > 1 ? func_get_arg(1) : [];
try {
$this->getEncoder($format, $context);
} catch (RuntimeException $e) {
@@ -58,14 +56,9 @@ class ChainEncoder implements EncoderInterface /*, ContextAwareEncoderInterface*
/**
* Checks whether the normalization is needed for the given format.
*
* @param string $format
*
* @return bool
*/
public function needsNormalization($format/*, array $context = []*/)
public function needsNormalization(string $format, array $context = []): bool
{
$context = \func_num_args() > 1 ? func_get_arg(1) : [];
$encoder = $this->getEncoder($format, $context);
if (!$encoder instanceof NormalizationAwareInterface) {
@@ -82,13 +75,9 @@ class ChainEncoder implements EncoderInterface /*, ContextAwareEncoderInterface*
/**
* Gets the encoder supporting the format.
*
* @param string $format
*
* @return EncoderInterface
*
* @throws RuntimeException if no encoder is found
*/
private function getEncoder($format, array $context)
private function getEncoder(string $format, array $context): EncoderInterface
{
if (isset($this->encoderByFormat[$format])
&& isset($this->encoders[$this->encoderByFormat[$format]])
@@ -96,9 +85,13 @@ class ChainEncoder implements EncoderInterface /*, ContextAwareEncoderInterface*
return $this->encoders[$this->encoderByFormat[$format]];
}
$cache = true;
foreach ($this->encoders as $i => $encoder) {
$cache = $cache && !$encoder instanceof ContextAwareEncoderInterface;
if ($encoder->supportsEncoding($format, $context)) {
$this->encoderByFormat[$format] = $i;
if ($cache) {
$this->encoderByFormat[$format] = $i;
}
return $encoder;
}
+102 -48
View File
@@ -12,6 +12,7 @@
namespace Symfony\Component\Serializer\Encoder;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
/**
* Encodes CSV data.
@@ -21,34 +22,55 @@ use Symfony\Component\Serializer\Exception\InvalidArgumentException;
*/
class CsvEncoder implements EncoderInterface, DecoderInterface
{
const FORMAT = 'csv';
const DELIMITER_KEY = 'csv_delimiter';
const ENCLOSURE_KEY = 'csv_enclosure';
const ESCAPE_CHAR_KEY = 'csv_escape_char';
const KEY_SEPARATOR_KEY = 'csv_key_separator';
const HEADERS_KEY = 'csv_headers';
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 OUTPUT_UTF8_BOM_KEY = 'output_utf8_bom';
private $delimiter;
private $enclosure;
private $escapeChar;
private $keySeparator;
private const UTF8_BOM = "\xEF\xBB\xBF";
private const FORMULAS_START_CHARACTERS = ['=', '-', '+', '@', "\t", "\r"];
private $defaultContext = [
self::DELIMITER_KEY => ',',
self::ENCLOSURE_KEY => '"',
self::ESCAPE_CHAR_KEY => '',
self::ESCAPE_FORMULAS_KEY => false,
self::HEADERS_KEY => [],
self::KEY_SEPARATOR_KEY => '.',
self::NO_HEADERS_KEY => false,
self::AS_COLLECTION_KEY => false,
self::OUTPUT_UTF8_BOM_KEY => false,
];
/**
* @param string $delimiter
* @param string $enclosure
* @param string $escapeChar
* @param string $keySeparator
* @param array $defaultContext
*/
public function __construct($delimiter = ',', $enclosure = '"', $escapeChar = '', $keySeparator = '.')
public function __construct($defaultContext = [], string $enclosure = '"', string $escapeChar = '', string $keySeparator = '.', bool $escapeFormulas = false)
{
if ('' === $escapeChar && \PHP_VERSION_ID < 70400) {
$escapeChar = '\\';
if (!\is_array($defaultContext)) {
@trigger_error('Passing configuration options directly to the constructor is deprecated since Symfony 4.2, use the default context instead.', \E_USER_DEPRECATED);
$defaultContext = [
self::DELIMITER_KEY => (string) $defaultContext,
self::ENCLOSURE_KEY => $enclosure,
self::ESCAPE_CHAR_KEY => $escapeChar,
self::KEY_SEPARATOR_KEY => $keySeparator,
self::ESCAPE_FORMULAS_KEY => $escapeFormulas,
];
}
$this->delimiter = $delimiter;
$this->enclosure = $enclosure;
$this->escapeChar = $escapeChar;
$this->keySeparator = $keySeparator;
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
if (\PHP_VERSION_ID < 70400 && '' === $this->defaultContext[self::ESCAPE_CHAR_KEY]) {
$this->defaultContext[self::ESCAPE_CHAR_KEY] = '\\';
}
}
/**
@@ -58,7 +80,7 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
{
$handle = fopen('php://temp,', 'w+');
if (!\is_array($data)) {
if (!is_iterable($data)) {
$data = [[$data]];
} elseif (empty($data)) {
$data = [[]];
@@ -75,18 +97,20 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
}
}
list($delimiter, $enclosure, $escapeChar, $keySeparator, $headers) = $this->getCsvOptions($context);
[$delimiter, $enclosure, $escapeChar, $keySeparator, $headers, $escapeFormulas, $outputBom] = $this->getCsvOptions($context);
foreach ($data as &$value) {
$flattened = [];
$this->flatten($value, $flattened, $keySeparator);
$this->flatten($value, $flattened, $keySeparator, '', $escapeFormulas);
$value = $flattened;
}
unset($value);
$headers = array_merge(array_values($headers), array_diff($this->extractHeaders($data), $headers));
fputcsv($handle, $headers, $delimiter, $enclosure, $escapeChar);
if (!($context[self::NO_HEADERS_KEY] ?? $this->defaultContext[self::NO_HEADERS_KEY])) {
fputcsv($handle, $headers, $delimiter, $enclosure, $escapeChar);
}
$headers = array_fill_keys($headers, '');
foreach ($data as $row) {
@@ -97,6 +121,14 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
$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;
}
@@ -117,12 +149,16 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
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 = [];
list($delimiter, $enclosure, $escapeChar, $keySeparator) = $this->getCsvOptions($context);
[$delimiter, $enclosure, $escapeChar, $keySeparator] = $this->getCsvOptions($context);
while (false !== ($cols = fgetcsv($handle, 0, $delimiter, $enclosure, $escapeChar))) {
$nbCols = \count($cols);
@@ -130,13 +166,20 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
if (null === $headers) {
$nbHeaders = $nbCols;
foreach ($cols as $col) {
$header = explode($keySeparator, $col);
$headers[] = $header;
$headerCount[] = \count($header);
}
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;
continue;
}
}
$item = [];
@@ -163,10 +206,18 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
}
fclose($handle);
if ($context[self::AS_COLLECTION_KEY] ?? $this->defaultContext[self::AS_COLLECTION_KEY]) {
return $result;
}
if (empty($result) || isset($result[1])) {
return $result;
}
if (!isset($context['as_collection'])) {
@trigger_error('Relying on the default value (false) of the "as_collection" option is deprecated since 4.2. You should set it to false explicitly instead as true will be the default value in 5.0.', \E_USER_DEPRECATED);
}
// 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];
}
@@ -181,41 +232,44 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
/**
* Flattens an array and generates keys including the path.
*
* @param string $keySeparator
* @param string $parentKey
*/
private function flatten(array $array, array &$result, $keySeparator, $parentKey = '')
private function flatten(iterable $array, array &$result, string $keySeparator, string $parentKey = '', bool $escapeFormulas = false)
{
foreach ($array as $key => $value) {
if (\is_array($value)) {
$this->flatten($value, $result, $keySeparator, $parentKey.$key.$keySeparator);
if (is_iterable($value)) {
$this->flatten($value, $result, $keySeparator, $parentKey.$key.$keySeparator, $escapeFormulas);
} else {
// Ensures an actual value is used when dealing with true and false
$result[$parentKey.$key] = false === $value ? 0 : (true === $value ? 1 : $value);
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)
private function getCsvOptions(array $context): array
{
$delimiter = isset($context[self::DELIMITER_KEY]) ? $context[self::DELIMITER_KEY] : $this->delimiter;
$enclosure = isset($context[self::ENCLOSURE_KEY]) ? $context[self::ENCLOSURE_KEY] : $this->enclosure;
$escapeChar = isset($context[self::ESCAPE_CHAR_KEY]) ? $context[self::ESCAPE_CHAR_KEY] : $this->escapeChar;
$keySeparator = isset($context[self::KEY_SEPARATOR_KEY]) ? $context[self::KEY_SEPARATOR_KEY] : $this->keySeparator;
$headers = isset($context[self::HEADERS_KEY]) ? $context[self::HEADERS_KEY] : [];
$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];
if (!\is_array($headers)) {
throw new InvalidArgumentException(sprintf('The "%s" context variable must be an array or null, given "%s".', self::HEADERS_KEY, \gettype($headers)));
}
return [$delimiter, $enclosure, $escapeChar, $keySeparator, $headers];
return [$delimiter, $enclosure, $escapeChar, $keySeparator, $headers, $escapeFormulas, $outputBom];
}
/**
* @return string[]
*/
private function extractHeaders(array $data)
private function extractHeaders(iterable $data): array
{
$headers = [];
$flippedHeaders = [];
+1 -1
View File
@@ -25,7 +25,7 @@ interface EncoderInterface
* @param string $format Format name
* @param array $context Options that normalizers/encoders have access to
*
* @return string|int|float|bool
* @return string
*
* @throws UnexpectedValueException
*/
+34 -30
View File
@@ -22,19 +22,41 @@ class JsonDecode implements DecoderInterface
{
protected $serializer;
private $associative;
private $recursionDepth;
/**
* True to return the result as an associative array, false for a nested stdClass hierarchy.
*/
public const ASSOCIATIVE = 'json_decode_associative';
public const OPTIONS = 'json_decode_options';
/**
* Specifies the recursion depth.
*/
public const RECURSION_DEPTH = 'json_decode_recursion_depth';
private $defaultContext = [
self::ASSOCIATIVE => false,
self::OPTIONS => 0,
self::RECURSION_DEPTH => 512,
];
/**
* Constructs a new JsonDecode instance.
*
* @param bool $associative True to return the result associative array, false for a nested stdClass hierarchy
* @param int $depth Specifies the recursion depth
* @param array $defaultContext
*/
public function __construct($associative = false, $depth = 512)
public function __construct($defaultContext = [], int $depth = 512)
{
$this->associative = $associative;
$this->recursionDepth = (int) $depth;
if (!\is_array($defaultContext)) {
@trigger_error(sprintf('Using constructor parameters that are not a default context is deprecated since Symfony 4.2, use the "%s" and "%s" keys of the context instead.', self::ASSOCIATIVE, self::RECURSION_DEPTH), \E_USER_DEPRECATED);
$defaultContext = [
self::ASSOCIATIVE => (bool) $defaultContext,
self::RECURSION_DEPTH => $depth,
];
}
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
}
/**
@@ -47,7 +69,7 @@ class JsonDecode implements DecoderInterface
* The $context array is a simple key=>value array, with the following supported keys:
*
* json_decode_associative: boolean
* If true, returns the object as associative array.
* 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
*
@@ -56,7 +78,7 @@ class JsonDecode implements DecoderInterface
* 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. Only supported with PHP 5.4.0 and higher
* Specifies additional options as per documentation for json_decode
*
* @return mixed
*
@@ -66,11 +88,9 @@ class JsonDecode implements DecoderInterface
*/
public function decode($data, $format, array $context = [])
{
$context = $this->resolveContext($context);
$associative = $context['json_decode_associative'];
$recursionDepth = $context['json_decode_recursion_depth'];
$options = $context['json_decode_options'];
$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);
@@ -96,20 +116,4 @@ class JsonDecode implements DecoderInterface
{
return JsonEncoder::FORMAT === $format;
}
/**
* Merges the default options of the Json Decoder with the passed context.
*
* @return array
*/
private function resolveContext(array $context)
{
$defaultOptions = [
'json_decode_associative' => $this->associative,
'json_decode_recursion_depth' => $this->recursionDepth,
'json_decode_options' => 0,
];
return array_merge($defaultOptions, $context);
}
}
+17 -15
View File
@@ -20,11 +20,24 @@ use Symfony\Component\Serializer\Exception\NotEncodableValueException;
*/
class JsonEncode implements EncoderInterface
{
private $options;
public const OPTIONS = 'json_encode_options';
public function __construct($bitmask = 0)
private $defaultContext = [
self::OPTIONS => 0,
];
/**
* @param array $defaultContext
*/
public function __construct($defaultContext = [])
{
$this->options = $bitmask;
if (!\is_array($defaultContext)) {
@trigger_error(sprintf('Passing an integer as first parameter of the "%s()" method is deprecated since Symfony 4.2, use the "json_encode_options" key of the context instead.', __METHOD__), \E_USER_DEPRECATED);
$this->defaultContext[self::OPTIONS] = (int) $defaultContext;
} else {
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
}
}
/**
@@ -34,8 +47,7 @@ class JsonEncode implements EncoderInterface
*/
public function encode($data, $format, array $context = [])
{
$context = $this->resolveContext($context);
$options = $context['json_encode_options'];
$options = $context[self::OPTIONS] ?? $this->defaultContext[self::OPTIONS];
try {
$encodedJson = json_encode($data, $options);
@@ -61,14 +73,4 @@ class JsonEncode implements EncoderInterface
{
return JsonEncoder::FORMAT === $format;
}
/**
* Merge default json encode options with context.
*
* @return array
*/
private function resolveContext(array $context = [])
{
return array_merge(['json_encode_options' => $this->options], $context);
}
}
+3 -3
View File
@@ -18,15 +18,15 @@ namespace Symfony\Component\Serializer\Encoder;
*/
class JsonEncoder implements EncoderInterface, DecoderInterface
{
const FORMAT = 'json';
public const FORMAT = 'json';
protected $encodingImpl;
protected $decodingImpl;
public function __construct(JsonEncode $encodingImpl = null, JsonDecode $decodingImpl = null)
{
$this->encodingImpl = $encodingImpl ?: new JsonEncode();
$this->decodingImpl = $decodingImpl ?: new JsonDecode(true);
$this->encodingImpl = $encodingImpl ?? new JsonEncode();
$this->decodingImpl = $decodingImpl ?? new JsonDecode([JsonDecode::ASSOCIATIVE => true]);
}
/**
@@ -1,27 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Encoder;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerAwareTrait;
/**
* SerializerAware Encoder implementation.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @deprecated since version 3.2, to be removed in 4.0. Use the SerializerAwareTrait instead.
*/
abstract class SerializerAwareEncoder implements SerializerAwareInterface
{
use SerializerAwareTrait;
}
+151 -153
View File
@@ -13,38 +13,76 @@ 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;
/**
* Encodes XML data.
*
* @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 extends SerializerAwareEncoder implements EncoderInterface, DecoderInterface, NormalizationAwareInterface
class XmlEncoder implements EncoderInterface, DecoderInterface, NormalizationAwareInterface, SerializerAwareInterface
{
const FORMAT = 'xml';
use SerializerAwareTrait;
public const FORMAT = 'xml';
public const AS_COLLECTION = 'as_collection';
/**
* @var \DOMDocument
* An array of ignored XML node types while decoding, each one of the DOM Predefined XML_* constants.
*/
private $dom;
private $format;
private $context;
private $rootNodeName = 'response';
private $loadOptions;
public const DECODER_IGNORED_NODE_TYPES = 'decoder_ignored_node_types';
/**
* Construct new XmlEncoder and allow to change the root node element name.
*
* @param string $rootNodeName
* @param int|null $loadOptions A bit field of LIBXML_* constants
* An array of ignored XML node types while encoding, each one of the DOM Predefined XML_* constants.
*/
public function __construct($rootNodeName = 'response', $loadOptions = null)
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.
*/
public const LOAD_OPTIONS = 'load_options';
public const REMOVE_EMPTY_TAGS = 'remove_empty_tags';
public const ROOT_NODE_NAME = 'xml_root_node_name';
public const STANDALONE = 'xml_standalone';
/** @deprecated The constant TYPE_CASE_ATTRIBUTES is deprecated since version 4.4 and will be removed in version 5. Use TYPE_CAST_ATTRIBUTES instead. */
public const TYPE_CASE_ATTRIBUTES = 'xml_type_cast_attributes';
public const TYPE_CAST_ATTRIBUTES = 'xml_type_cast_attributes';
public const VERSION = 'xml_version';
private $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::REMOVE_EMPTY_TAGS => false,
self::ROOT_NODE_NAME => 'response',
self::TYPE_CAST_ATTRIBUTES => true,
];
/**
* @param array $defaultContext
*/
public function __construct($defaultContext = [], int $loadOptions = null, array $decoderIgnoredNodeTypes = [\XML_PI_NODE, \XML_COMMENT_NODE], array $encoderIgnoredNodeTypes = [])
{
$this->rootNodeName = $rootNodeName;
$this->loadOptions = null !== $loadOptions ? $loadOptions : \LIBXML_NONET | \LIBXML_NOBLANKS;
if (!\is_array($defaultContext)) {
@trigger_error('Passing configuration options directly to the constructor is deprecated since Symfony 4.2, use the default context instead.', \E_USER_DEPRECATED);
$defaultContext = [
self::DECODER_IGNORED_NODE_TYPES => $decoderIgnoredNodeTypes,
self::ENCODER_IGNORED_NODE_TYPES => $encoderIgnoredNodeTypes,
self::LOAD_OPTIONS => $loadOptions ?? \LIBXML_NONET | \LIBXML_NOBLANKS,
self::ROOT_NODE_NAME => (string) $defaultContext,
];
}
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
}
/**
@@ -52,25 +90,25 @@ class XmlEncoder extends SerializerAwareEncoder implements EncoderInterface, Dec
*/
public function encode($data, $format, array $context = [])
{
$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();
return $data->saveXML($ignorePiNode ? $data->documentElement : null);
}
$xmlRootNodeName = $this->resolveXmlRootName($context);
$xmlRootNodeName = $context[self::ROOT_NODE_NAME] ?? $this->defaultContext[self::ROOT_NODE_NAME];
$this->dom = $this->createDomDocument($context);
$this->format = $format;
$this->context = $context;
$dom = $this->createDomDocument($context);
if (null !== $data && !is_scalar($data)) {
$root = $this->dom->createElement($xmlRootNodeName);
$this->dom->appendChild($root);
$this->buildXml($root, $data, $xmlRootNodeName);
if (null !== $data && !\is_scalar($data)) {
$root = $dom->createElement($xmlRootNodeName);
$dom->appendChild($root);
$this->buildXml($root, $data, $format, $context, $xmlRootNodeName);
} else {
$this->appendNode($this->dom, $data, $xmlRootNodeName);
$this->appendNode($dom, $data, $format, $context, $xmlRootNodeName);
}
return $this->dom->saveXML();
return $dom->saveXML($ignorePiNode ? $dom->documentElement : null);
}
/**
@@ -89,7 +127,7 @@ class XmlEncoder extends SerializerAwareEncoder implements EncoderInterface, Dec
libxml_clear_errors();
$dom = new \DOMDocument();
$dom->loadXML($data, $this->loadOptions);
$dom->loadXML($data, $context[self::LOAD_OPTIONS] ?? $this->defaultContext[self::LOAD_OPTIONS]);
libxml_use_internal_errors($internalErrors);
if (\LIBXML_VERSION < 20900) {
@@ -103,11 +141,15 @@ class XmlEncoder extends SerializerAwareEncoder implements EncoderInterface, Dec
}
$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 && \XML_PI_NODE !== $child->nodeType) {
if (!$rootNode) {
$rootNode = $child;
}
}
@@ -164,32 +206,35 @@ class XmlEncoder extends SerializerAwareEncoder implements EncoderInterface, Dec
/**
* Sets the root node name.
*
* @deprecated since Symfony 4.2
*
* @param string $name Root node name
*/
public function setRootNodeName($name)
{
$this->rootNodeName = $name;
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the context instead.', __METHOD__), \E_USER_DEPRECATED);
$this->defaultContext[self::ROOT_NODE_NAME] = $name;
}
/**
* Returns the root node name.
*
* @deprecated since Symfony 4.2
*
* @return string
*/
public function getRootNodeName()
{
return $this->rootNodeName;
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the context instead.', __METHOD__), \E_USER_DEPRECATED);
return $this->defaultContext[self::ROOT_NODE_NAME];
}
/**
* @param string $val
*
* @return bool
*/
final protected function appendXMLString(\DOMNode $node, $val)
final protected function appendXMLString(\DOMNode $node, string $val): bool
{
if (\strlen($val) > 0) {
$frag = $this->dom->createDocumentFragment();
if ('' !== $val) {
$frag = $node->ownerDocument->createDocumentFragment();
$frag->appendXML($val);
$node->appendChild($frag);
@@ -199,27 +244,17 @@ class XmlEncoder extends SerializerAwareEncoder implements EncoderInterface, Dec
return false;
}
/**
* @param string $val
*
* @return bool
*/
final protected function appendText(\DOMNode $node, $val)
final protected function appendText(\DOMNode $node, string $val): bool
{
$nodeText = $this->dom->createTextNode($val);
$nodeText = $node->ownerDocument->createTextNode($val);
$node->appendChild($nodeText);
return true;
}
/**
* @param string $val
*
* @return bool
*/
final protected function appendCData(\DOMNode $node, $val)
final protected function appendCData(\DOMNode $node, string $val): bool
{
$nodeText = $this->dom->createCDATASection($val);
$nodeText = $node->ownerDocument->createCDATASection($val);
$node->appendChild($nodeText);
return true;
@@ -227,10 +262,8 @@ class XmlEncoder extends SerializerAwareEncoder implements EncoderInterface, Dec
/**
* @param \DOMDocumentFragment $fragment
*
* @return bool
*/
final protected function appendDocumentFragment(\DOMNode $node, $fragment)
final protected function appendDocumentFragment(\DOMNode $node, $fragment): bool
{
if ($fragment instanceof \DOMDocumentFragment) {
$node->appendChild($fragment);
@@ -241,17 +274,20 @@ class XmlEncoder extends SerializerAwareEncoder implements EncoderInterface, Dec
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.
*
* @param string $name
*
* @return bool
*/
final protected function isElementNameValid($name)
final protected function isElementNameValid(string $name): bool
{
return $name &&
false === strpos($name, ' ') &&
!str_contains($name, ' ') &&
preg_match('#^[\pL_][\pL0-9._:-]*$#ui', $name);
}
@@ -291,17 +327,15 @@ class XmlEncoder extends SerializerAwareEncoder implements EncoderInterface, Dec
/**
* Parse the input DOMNode attributes into an array.
*
* @return array
*/
private function parseXmlAttributes(\DOMNode $node, array $context = [])
private function parseXmlAttributes(\DOMNode $node, array $context = []): array
{
if (!$node->hasAttributes()) {
return [];
}
$data = [];
$typeCastAttributes = $this->resolveXmlTypeCastAttributes($context);
$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])) {
@@ -338,27 +372,24 @@ class XmlEncoder extends SerializerAwareEncoder implements EncoderInterface, Dec
}
$value = [];
$decoderIgnoredNodeTypes = $context[self::DECODER_IGNORED_NODE_TYPES] ?? $this->defaultContext[self::DECODER_IGNORED_NODE_TYPES];
foreach ($node->childNodes as $subnode) {
if (\XML_PI_NODE === $subnode->nodeType) {
if (\in_array($subnode->nodeType, $decoderIgnoredNodeTypes, true)) {
continue;
}
$val = $this->parseXml($subnode, $context);
if ('item' === $subnode->nodeName && isset($val['@key'])) {
if (isset($val['#'])) {
$value[$val['@key']] = $val['#'];
} else {
$value[$val['@key']] = $val;
}
$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 (\is_array($val) && 1 === \count($val)) {
if (!$asCollection && \is_array($val) && 1 === \count($val)) {
$value[$key] = current($val);
}
}
@@ -370,26 +401,32 @@ class XmlEncoder extends SerializerAwareEncoder implements EncoderInterface, Dec
* Parse the data and convert it to DOMElements.
*
* @param array|object $data
* @param string|null $xmlRootNodeName
*
* @return bool
*
* @throws NotEncodableValueException
*/
private function buildXml(\DOMNode $parentNode, $data, $xmlRootNodeName = null)
private function buildXml(\DOMNode $parentNode, $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, $this->format)))) {
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 (0 === strpos($key, '@') && $this->isElementNameValid($attributeName = substr($key, 1))) {
if (!is_scalar($data)) {
$data = $this->serializer->normalize($data, $this->format, $this->context);
// 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);
$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)))) {
@@ -399,15 +436,15 @@ class XmlEncoder extends SerializerAwareEncoder implements EncoderInterface, Dec
* From ["item" => [0,1]];.
*/
foreach ($data as $subData) {
$append = $this->appendNode($parentNode, $subData, $key);
$append = $this->appendNode($parentNode, $subData, $format, $context, $key);
}
} else {
$append = $this->appendNode($parentNode, $data, $key);
$append = $this->appendNode($parentNode, $data, $format, $context, $key);
}
} elseif (is_numeric($key) || !$this->isElementNameValid($key)) {
$append = $this->appendNode($parentNode, $data, 'item', $key);
} elseif (null !== $data || !isset($this->context['remove_empty_tags']) || false === $this->context['remove_empty_tags']) {
$append = $this->appendNode($parentNode, $data, $key);
$append = $this->appendNode($parentNode, $data, $format, $context, 'item', $key);
} elseif (null !== $data || !$removeEmptyTags) {
$append = $this->appendNode($parentNode, $data, $format, $context, $key);
}
}
@@ -419,9 +456,9 @@ class XmlEncoder extends SerializerAwareEncoder implements EncoderInterface, Dec
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, $this->format, $this->context);
if (null !== $data && !is_scalar($data)) {
return $this->buildXml($parentNode, $data, $xmlRootNodeName);
$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
@@ -429,10 +466,10 @@ class XmlEncoder extends SerializerAwareEncoder implements EncoderInterface, Dec
$root = $parentNode->parentNode;
$root->removeChild($parentNode);
return $this->appendNode($root, $data, $xmlRootNodeName);
return $this->appendNode($root, $data, $format, $context, $xmlRootNodeName);
}
return $this->appendNode($parentNode, $data, 'data');
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))));
@@ -442,18 +479,15 @@ class XmlEncoder extends SerializerAwareEncoder implements EncoderInterface, Dec
* Selects the type of node to create and appends it to the parent.
*
* @param array|object $data
* @param string $nodeName
* @param string $key
*
* @return bool
*/
private function appendNode(\DOMNode $parentNode, $data, $nodeName, $key = null)
private function appendNode(\DOMNode $parentNode, $data, string $format, array $context, string $nodeName, string $key = null): bool
{
$node = $this->dom->createElement($nodeName);
$dom = $parentNode instanceof \DOMDocument ? $parentNode : $parentNode->ownerDocument;
$node = $dom->createElement($nodeName);
if (null !== $key) {
$node->setAttribute('key', $key);
}
$appendNode = $this->selectNodeType($node, $data);
$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);
@@ -464,43 +498,35 @@ class XmlEncoder extends SerializerAwareEncoder implements EncoderInterface, Dec
/**
* Checks if a value contains any characters which would require CDATA wrapping.
*
* @param string $val
*
* @return bool
*/
private function needsCdataWrapping($val)
private function needsCdataWrapping(string $val): bool
{
return 0 < preg_match('/[<>&]/', $val);
return preg_match('/[<>&]/', $val);
}
/**
* Tests the value being passed and decide what sort of element to create.
*
* @param mixed $val
*
* @return bool
*
* @throws NotEncodableValueException
*/
private function selectNodeType(\DOMNode $node, $val)
private function selectNodeType(\DOMNode $node, $val, string $format, array $context): bool
{
if (\is_array($val)) {
return $this->buildXml($node, $val);
return $this->buildXml($node, $val, $format, $context);
} elseif ($val instanceof \SimpleXMLElement) {
$child = $this->dom->importNode(dom_import_simplexml($val), true);
$child = $node->ownerDocument->importNode(dom_import_simplexml($val), true);
$node->appendChild($child);
} elseif ($val instanceof \Traversable) {
$this->buildXml($node, $val);
$this->buildXml($node, $val, $format, $context);
} elseif ($val instanceof \DOMNode) {
$child = $this->dom->importNode($val, true);
$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, $this->format, $this->context));
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)) {
@@ -514,55 +540,27 @@ class XmlEncoder extends SerializerAwareEncoder implements EncoderInterface, Dec
return true;
}
/**
* Get real XML root node name, taking serializer options into account.
*
* @return string
*/
private function resolveXmlRootName(array $context = [])
{
return isset($context['xml_root_node_name'])
? $context['xml_root_node_name']
: $this->rootNodeName;
}
/**
* Get XML option for type casting attributes Defaults to true.
*
* @return bool
*/
private function resolveXmlTypeCastAttributes(array $context = [])
{
return isset($context['xml_type_cast_attributes'])
? (bool) $context['xml_type_cast_attributes']
: true;
}
/**
* Create a DOM document, taking serializer options into account.
*
* @param array $context Options that the encoder has access to
*
* @return \DOMDocument
*/
private function createDomDocument(array $context)
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
'xml_format_output' => 'formatOutput',
self::FORMAT_OUTPUT => 'formatOutput',
// the version number of the document
'xml_version' => 'xmlVersion',
self::VERSION => 'xmlVersion',
// the encoding of the document
'xml_encoding' => 'encoding',
self::ENCODING => 'encoding',
// whether the document is standalone
'xml_standalone' => 'xmlStandalone',
self::STANDALONE => 'xmlStandalone',
];
foreach ($xmlOptions as $xmlOption => $documentProperty) {
if (isset($context[$xmlOption])) {
$document->$documentProperty = $context[$xmlOption];
if ($contextOption = $context[$xmlOption] ?? $this->defaultContext[$xmlOption] ?? false) {
$document->$documentProperty = $contextOption;
}
}
+13 -5
View File
@@ -14,6 +14,7 @@ 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.
@@ -22,7 +23,10 @@ use Symfony\Component\Yaml\Parser;
*/
class YamlEncoder implements EncoderInterface, DecoderInterface
{
const FORMAT = 'yaml';
public const FORMAT = 'yaml';
private const ALTERNATIVE_FORMAT = 'yml';
public const PRESERVE_EMPTY_OBJECTS = 'preserve_empty_objects';
private $dumper;
private $parser;
@@ -34,8 +38,8 @@ class YamlEncoder implements EncoderInterface, DecoderInterface
throw new RuntimeException('The YamlEncoder class requires the "Yaml" component. Install "symfony/yaml" to use it.');
}
$this->dumper = $dumper ?: new Dumper();
$this->parser = $parser ?: new Parser();
$this->dumper = $dumper ?? new Dumper();
$this->parser = $parser ?? new Parser();
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
}
@@ -46,6 +50,10 @@ class YamlEncoder implements EncoderInterface, DecoderInterface
{
$context = array_merge($this->defaultContext, $context);
if (isset($context[self::PRESERVE_EMPTY_OBJECTS])) {
$context['yaml_flags'] |= Yaml::DUMP_OBJECT_AS_MAP;
}
return $this->dumper->dump($data, $context['yaml_inline'], $context['yaml_indent'], $context['yaml_flags']);
}
@@ -54,7 +62,7 @@ class YamlEncoder implements EncoderInterface, DecoderInterface
*/
public function supportsEncoding($format)
{
return self::FORMAT === $format;
return self::FORMAT === $format || self::ALTERNATIVE_FORMAT === $format;
}
/**
@@ -72,6 +80,6 @@ class YamlEncoder implements EncoderInterface, DecoderInterface
*/
public function supportsDecoding($format)
{
return self::FORMAT === $format;
return self::FORMAT === $format || self::ALTERNATIVE_FORMAT === $format;
}
}
+1 -1
View File
@@ -16,6 +16,6 @@ namespace Symfony\Component\Serializer\Exception;
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
interface ExceptionInterface
interface ExceptionInterface extends \Throwable
{
}
@@ -20,7 +20,7 @@ class ExtraAttributesException extends RuntimeException
{
private $extraAttributes;
public function __construct(array $extraAttributes, \Exception $previous = null)
public function __construct(array $extraAttributes, \Throwable $previous = null)
{
$msg = sprintf('Extra attributes are not allowed ("%s" are unknown).', implode('", "', $extraAttributes));
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2004-2020 Fabien Potencier
Copyright (c) 2004-2022 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
+31 -6
View File
@@ -42,11 +42,15 @@ class AttributeMetadata implements AttributeMetadataInterface
public $maxDepth;
/**
* Constructs a metadata for the given attribute.
* @var string|null
*
* @param string $name
* @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 function __construct($name)
public $serializedName;
public function __construct(string $name)
{
$this->name = $name;
}
@@ -54,7 +58,7 @@ class AttributeMetadata implements AttributeMetadataInterface
/**
* {@inheritdoc}
*/
public function getName()
public function getName(): string
{
return $this->name;
}
@@ -72,7 +76,7 @@ class AttributeMetadata implements AttributeMetadataInterface
/**
* {@inheritdoc}
*/
public function getGroups()
public function getGroups(): array
{
return $this->groups;
}
@@ -93,6 +97,22 @@ class AttributeMetadata implements AttributeMetadataInterface
return $this->maxDepth;
}
/**
* {@inheritdoc}
*/
public function setSerializedName(string $serializedName = null)
{
$this->serializedName = $serializedName;
}
/**
* {@inheritdoc}
*/
public function getSerializedName(): ?string
{
return $this->serializedName;
}
/**
* {@inheritdoc}
*/
@@ -106,6 +126,11 @@ class AttributeMetadata implements AttributeMetadataInterface
if (null === $this->maxDepth) {
$this->maxDepth = $attributeMetadata->getMaxDepth();
}
// Overwrite only if not defined
if (null === $this->serializedName) {
$this->serializedName = $attributeMetadata->getSerializedName();
}
}
/**
@@ -115,6 +140,6 @@ class AttributeMetadata implements AttributeMetadataInterface
*/
public function __sleep()
{
return ['name', 'groups', 'maxDepth'];
return ['name', 'groups', 'maxDepth', 'serializedName'];
}
}
@@ -24,10 +24,8 @@ interface AttributeMetadataInterface
{
/**
* Gets the attribute name.
*
* @return string
*/
public function getName();
public function getName(): string;
/**
* Adds this attribute to the given group.
@@ -41,7 +39,7 @@ interface AttributeMetadataInterface
*
* @return string[]
*/
public function getGroups();
public function getGroups(): array;
/**
* Sets the serialization max depth for this attribute.
@@ -57,6 +55,16 @@ interface AttributeMetadataInterface
*/
public function getMaxDepth();
/**
* Sets the serialization name for this attribute.
*/
public function setSerializedName(string $serializedName = null);
/**
* Gets the serialization name for this attribute.
*/
public function getSerializedName(): ?string;
/**
* Merges an {@see AttributeMetadataInterface} with in the current one.
*/
+31 -6
View File
@@ -40,19 +40,27 @@ class ClassMetadata implements ClassMetadataInterface
private $reflClass;
/**
* Constructs a metadata for the given class.
* @var ClassDiscriminatorMapping|null
*
* @param string $class
* @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 function __construct($class)
public $classDiscriminatorMapping;
/**
* Constructs a metadata for the given class.
*/
public function __construct(string $class, ClassDiscriminatorMapping $classDiscriminatorMapping = null)
{
$this->name = $class;
$this->classDiscriminatorMapping = $classDiscriminatorMapping;
}
/**
* {@inheritdoc}
*/
public function getName()
public function getName(): string
{
return $this->name;
}
@@ -68,7 +76,7 @@ class ClassMetadata implements ClassMetadataInterface
/**
* {@inheritdoc}
*/
public function getAttributesMetadata()
public function getAttributesMetadata(): array
{
return $this->attributesMetadata;
}
@@ -90,7 +98,7 @@ class ClassMetadata implements ClassMetadataInterface
/**
* {@inheritdoc}
*/
public function getReflectionClass()
public function getReflectionClass(): \ReflectionClass
{
if (!$this->reflClass) {
$this->reflClass = new \ReflectionClass($this->getName());
@@ -99,6 +107,22 @@ class ClassMetadata implements ClassMetadataInterface
return $this->reflClass;
}
/**
* {@inheritdoc}
*/
public function getClassDiscriminatorMapping(): ?ClassDiscriminatorMapping
{
return $this->classDiscriminatorMapping;
}
/**
* {@inheritdoc}
*/
public function setClassDiscriminatorMapping(ClassDiscriminatorMapping $mapping = null)
{
$this->classDiscriminatorMapping = $mapping;
}
/**
* Returns the names of the properties that should be serialized.
*
@@ -109,6 +133,7 @@ class ClassMetadata implements ClassMetadataInterface
return [
'name',
'attributesMetadata',
'classDiscriminatorMapping',
];
}
}
@@ -29,7 +29,7 @@ interface ClassMetadataInterface
*
* @return string The name of the backing class
*/
public function getName();
public function getName(): string;
/**
* Adds an {@link AttributeMetadataInterface}.
@@ -41,7 +41,7 @@ interface ClassMetadataInterface
*
* @return AttributeMetadataInterface[]
*/
public function getAttributesMetadata();
public function getAttributesMetadata(): array;
/**
* Merges a {@link ClassMetadataInterface} in the current one.
@@ -50,8 +50,10 @@ interface ClassMetadataInterface
/**
* Returns a {@link \ReflectionClass} instance for this class.
*
* @return \ReflectionClass
*/
public function getReflectionClass();
public function getReflectionClass(): \ReflectionClass;
public function getClassDiscriminatorMapping(): ?ClassDiscriminatorMapping;
public function setClassDiscriminatorMapping(ClassDiscriminatorMapping $mapping = null);
}
@@ -44,8 +44,7 @@ class CacheClassMetadataFactory implements ClassMetadataFactoryInterface
public function getMetadataFor($value)
{
$class = $this->getClass($value);
// Key cannot contain backslashes according to PSR-6
$key = strtr($class, '\\', '_');
$key = rawurlencode(strtr($class, '\\', '_'));
$item = $this->cacheItemPool->getItem($key);
if ($item->isHit()) {
@@ -11,8 +11,6 @@
namespace Symfony\Component\Serializer\Mapping\Factory;
use Doctrine\Common\Cache\Cache;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Mapping\ClassMetadata;
use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface;
@@ -26,17 +24,15 @@ class ClassMetadataFactory implements ClassMetadataFactoryInterface
use ClassResolverTrait;
private $loader;
private $cache;
/**
* @var array
*/
private $loadedClasses;
public function __construct(LoaderInterface $loader, Cache $cache = null)
public function __construct(LoaderInterface $loader)
{
$this->loader = $loader;
$this->cache = $cache;
if (null !== $cache) {
@trigger_error(sprintf('Passing a Doctrine Cache instance as 2nd parameter of the "%s" constructor is deprecated since Symfony 3.1. This parameter will be removed in Symfony 4.0. Use the "%s" class instead.', __CLASS__, CacheClassMetadataFactory::class), \E_USER_DEPRECATED);
}
}
/**
@@ -50,10 +46,6 @@ class ClassMetadataFactory implements ClassMetadataFactoryInterface
return $this->loadedClasses[$class];
}
if ($this->cache && ($this->loadedClasses[$class] = $this->cache->fetch($class))) {
return $this->loadedClasses[$class];
}
$classMetadata = new ClassMetadata($class);
$this->loader->loadClassMetadata($classMetadata);
@@ -69,10 +61,6 @@ class ClassMetadataFactory implements ClassMetadataFactoryInterface
$classMetadata->merge($this->getMetadataFor($interface->name));
}
if ($this->cache) {
$this->cache->save($class, $classMetadata);
}
return $this->loadedClasses[$class] = $classMetadata;
}
@@ -81,14 +69,6 @@ class ClassMetadataFactory implements ClassMetadataFactoryInterface
*/
public function hasMetadataFor($value)
{
try {
$this->getClass($value);
return true;
} catch (InvalidArgumentException $invalidArgumentException) {
// Return false in case of exception
}
return false;
return \is_object($value) || (\is_string($value) && (class_exists($value) || interface_exists($value, false)));
}
}
@@ -27,14 +27,12 @@ trait ClassResolverTrait
*
* @param object|string $value
*
* @return string
*
* @throws InvalidArgumentException If the class does not exist
*/
private function getClass($value)
private function getClass($value): string
{
if (\is_string($value)) {
if (!class_exists($value) && !interface_exists($value)) {
if (!class_exists($value) && !interface_exists($value, false)) {
throw new InvalidArgumentException(sprintf('The class or interface "%s" does not exist.', $value));
}
@@ -12,10 +12,13 @@
namespace Symfony\Component\Serializer\Mapping\Loader;
use Doctrine\Common\Annotations\Reader;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\MaxDepth;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Serializer\Exception\MappingException;
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping;
use Symfony\Component\Serializer\Mapping\ClassMetadataInterface;
/**
@@ -43,6 +46,15 @@ class AnnotationLoader implements LoaderInterface
$attributesMetadata = $classMetadata->getAttributesMetadata();
foreach ($this->reader->getClassAnnotations($reflectionClass) as $annotation) {
if ($annotation instanceof DiscriminatorMap) {
$classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping(
$annotation->getTypeProperty(),
$annotation->getMapping()
));
}
}
foreach ($reflectionClass->getProperties() as $property) {
if (!isset($attributesMetadata[$property->name])) {
$attributesMetadata[$property->name] = new AttributeMetadata($property->name);
@@ -57,6 +69,8 @@ class AnnotationLoader implements LoaderInterface
}
} elseif ($annotation instanceof MaxDepth) {
$attributesMetadata[$property->name]->setMaxDepth($annotation->getMaxDepth());
} elseif ($annotation instanceof SerializedName) {
$attributesMetadata[$property->name]->setSerializedName($annotation->getSerializedName());
}
$loaded = true;
@@ -84,7 +98,7 @@ class AnnotationLoader implements LoaderInterface
foreach ($this->reader->getMethodAnnotations($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));
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) {
@@ -92,10 +106,16 @@ class AnnotationLoader implements LoaderInterface
}
} 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));
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());
}
$loaded = true;
+1 -1
View File
@@ -27,7 +27,7 @@ abstract class FileLoader implements LoaderInterface
*
* @throws MappingException if the mapping file does not exist or is not readable
*/
public function __construct($file)
public function __construct(string $file)
{
if (!is_file($file)) {
throw new MappingException(sprintf('The mapping file "%s" does not exist.', $file));
+21 -7
View File
@@ -14,6 +14,7 @@ namespace Symfony\Component\Serializer\Mapping\Loader;
use Symfony\Component\Config\Util\XmlUtils;
use Symfony\Component\Serializer\Exception\MappingException;
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping;
use Symfony\Component\Serializer\Mapping\ClassMetadataInterface;
/**
@@ -65,6 +66,23 @@ class XmlFileLoader extends FileLoader
if (isset($attribute['max-depth'])) {
$attributeMetadata->setMaxDepth((int) $attribute['max-depth']);
}
if (isset($attribute['serialized-name'])) {
$attributeMetadata->setSerializedName((string) $attribute['serialized-name']);
}
}
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;
@@ -88,15 +106,11 @@ class XmlFileLoader extends FileLoader
}
/**
* Parses a XML File.
*
* @param string $file Path of file
*
* @return \SimpleXMLElement
* Parses an XML File.
*
* @throws MappingException
*/
private function parseFile($file)
private function parseFile(string $file): \SimpleXMLElement
{
try {
$dom = XmlUtils::loadFile($file, __DIR__.'/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd');
@@ -107,7 +121,7 @@ class XmlFileLoader extends FileLoader
return simplexml_import_dom($dom);
}
private function getClassesFromXml()
private function getClassesFromXml(): array
{
$xml = $this->parseFile($this->file);
$classes = [];
+27 -2
View File
@@ -13,8 +13,10 @@ namespace Symfony\Component\Serializer\Mapping\Loader;
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.
@@ -83,9 +85,32 @@ class YamlFileLoader extends FileLoader
$attributeMetadata->setMaxDepth($data['max_depth']);
}
if (isset($data['serialized_name'])) {
if (!\is_string($data['serialized_name']) || empty($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($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;
}
@@ -103,7 +128,7 @@ class YamlFileLoader extends FileLoader
return array_keys($this->classes);
}
private function getClassesFromYaml()
private function getClassesFromYaml(): array
{
if (!stream_is_local($this->file)) {
throw new MappingException(sprintf('This is not a local file "%s".', $this->file));
@@ -113,7 +138,7 @@ class YamlFileLoader extends FileLoader
$this->yamlParser = new Parser();
}
$classes = $this->yamlParser->parseFile($this->file);
$classes = $this->yamlParser->parseFile($this->file, Yaml::PARSE_CONSTANT);
if (empty($classes)) {
return [];
@@ -8,7 +8,7 @@
<xsd:annotation>
<xsd:documentation><![CDATA[
Symfony Serializer Mapping Schema, version 1.0
Authors: Kévin Dunglas
Authors: Kévin Dunglas, Samuel Roze
A serializer mapping connects attributes with serialization groups.
]]></xsd:documentation>
@@ -37,10 +37,23 @@
</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[
@@ -58,6 +71,13 @@
</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:complexType>
</xsd:schema>
@@ -25,7 +25,7 @@ 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(array $attributes = null, $lowerCamelCase = true)
public function __construct(array $attributes = null, bool $lowerCamelCase = true)
{
$this->attributes = $attributes;
$this->lowerCamelCase = $lowerCamelCase;
+244 -73
View File
@@ -14,34 +14,123 @@ namespace Symfony\Component\Serializer\Normalizer;
use Symfony\Component\Serializer\Exception\CircularReferenceException;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
use Symfony\Component\Serializer\Exception\RuntimeException;
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerAwareTrait;
/**
* Normalizer implementation.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
abstract class AbstractNormalizer extends SerializerAwareNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface, CacheableSupportsMethodInterface
{
use ObjectToPopulateTrait;
use SerializerAwareTrait;
const CIRCULAR_REFERENCE_LIMIT = 'circular_reference_limit';
const OBJECT_TO_POPULATE = 'object_to_populate';
const GROUPS = 'groups';
const ATTRIBUTES = 'attributes';
const ALLOW_EXTRA_ATTRIBUTES = 'allow_extra_attributes';
/* constants to configure the context */
/**
* @var int
* How many loops of circular reference to allow while normalizing.
*
* The default value of 1 means that when we encounter the same object a
* second time, we consider that a circular reference.
*
* You can raise this value for special cases, e.g. in combination with the
* max depth setting of the object normalizer.
*/
public const CIRCULAR_REFERENCE_LIMIT = 'circular_reference_limit';
/**
* Instead of creating a new instance of an object, update the specified object.
*
* If you have a nested structure, child objects will be overwritten with
* new instances unless you set DEEP_OBJECT_TO_POPULATE to true.
*/
public const OBJECT_TO_POPULATE = 'object_to_populate';
/**
* Only (de)normalize attributes that are in the specified groups.
*/
public const GROUPS = 'groups';
/**
* Limit (de)normalize to the specified names.
*
* For nested structures, this list needs to reflect the object tree.
*/
public const ATTRIBUTES = 'attributes';
/**
* If ATTRIBUTES are specified, and the source has fields that are not part of that list,
* either ignore those attributes (true) or throw an ExtraAttributesException (false).
*/
public const ALLOW_EXTRA_ATTRIBUTES = 'allow_extra_attributes';
/**
* Hashmap of default values for constructor arguments.
*
* The names need to match the parameter names in the constructor arguments.
*/
public const DEFAULT_CONSTRUCTOR_ARGUMENTS = 'default_constructor_arguments';
/**
* Hashmap of field name => callable to (de)normalize this field.
*
* The callable is called if the field is encountered with the arguments:
*
* - mixed $attributeValue value of this field
* - object|string $object the whole object being normalized or the object's class being denormalized
* - string $attributeName name of the attribute being (de)normalized
* - string $format the requested format
* - array $context the serialization context
*/
public const CALLBACKS = 'callbacks';
/**
* Handler to call when a circular reference has been detected.
*
* If you specify no handler, a CircularReferenceException is thrown.
*
* The method will be called with ($object, $format, $context) and its
* return value is returned as the result of the normalize call.
*/
public const CIRCULAR_REFERENCE_HANDLER = 'circular_reference_handler';
/**
* Skip the specified attributes when normalizing an object tree.
*
* This list is applied to each element of nested structures.
*
* Note: The behaviour for nested structures is different from ATTRIBUTES
* for historical reason. Aligning the behaviour would be a BC break.
*/
public const IGNORED_ATTRIBUTES = 'ignored_attributes';
/**
* @internal
*/
protected const CIRCULAR_REFERENCE_LIMIT_COUNTERS = 'circular_reference_limit_counters';
protected $defaultContext = [
self::ALLOW_EXTRA_ATTRIBUTES => true,
self::CIRCULAR_REFERENCE_LIMIT => 1,
self::IGNORED_ATTRIBUTES => [],
];
/**
* @deprecated since Symfony 4.2
*/
protected $circularReferenceLimit = 1;
/**
* @var callable
* @deprecated since Symfony 4.2
*
* @var callable|null
*/
protected $circularReferenceHandler;
@@ -56,31 +145,40 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
protected $nameConverter;
/**
* @var array
* @deprecated since Symfony 4.2
*/
protected $callbacks = [];
/**
* @var array
* @deprecated since Symfony 4.2
*/
protected $ignoredAttributes = [];
/**
* @var array
* @deprecated since Symfony 4.2
*/
protected $camelizedAttributes = [];
/**
* Sets the {@link ClassMetadataFactoryInterface} to use.
*/
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null)
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, array $defaultContext = [])
{
$this->classMetadataFactory = $classMetadataFactory;
$this->nameConverter = $nameConverter;
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
$this->validateCallbackContext($this->defaultContext, 'default');
if (isset($this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER]) && !\is_callable($this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER])) {
throw new InvalidArgumentException(sprintf('Invalid callback found in the "%s" default context option.', self::CIRCULAR_REFERENCE_HANDLER));
}
}
/**
* Set circular reference limit.
* Sets circular reference limit.
*
* @deprecated since Symfony 4.2
*
* @param int $circularReferenceLimit Limit of iterations for the same object
*
@@ -88,27 +186,35 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
*/
public function setCircularReferenceLimit($circularReferenceLimit)
{
$this->circularReferenceLimit = $circularReferenceLimit;
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the "circular_reference_limit" key of the context instead.', __METHOD__), \E_USER_DEPRECATED);
$this->defaultContext[self::CIRCULAR_REFERENCE_LIMIT] = $this->circularReferenceLimit = $circularReferenceLimit;
return $this;
}
/**
* Set circular reference handler.
* Sets circular reference handler.
*
* @deprecated since Symfony 4.2
*
* @return self
*/
public function setCircularReferenceHandler(callable $circularReferenceHandler)
{
$this->circularReferenceHandler = $circularReferenceHandler;
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the "circular_reference_handler" key of the context instead.', __METHOD__), \E_USER_DEPRECATED);
$this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER] = $this->circularReferenceHandler = $circularReferenceHandler;
return $this;
}
/**
* Set normalization callbacks.
* Sets (de)normalization callbacks.
*
* @param callable[] $callbacks Help normalize the result
* @deprecated since Symfony 4.2
*
* @param callable[] $callbacks Help (de)normalize the result
*
* @return self
*
@@ -116,28 +222,42 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
*/
public function setCallbacks(array $callbacks)
{
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the "callbacks" key of the context instead.', __METHOD__), \E_USER_DEPRECATED);
foreach ($callbacks as $attribute => $callback) {
if (!\is_callable($callback)) {
throw new InvalidArgumentException(sprintf('The given callback for attribute "%s" is not callable.', $attribute));
}
}
$this->callbacks = $callbacks;
$this->defaultContext[self::CALLBACKS] = $this->callbacks = $callbacks;
return $this;
}
/**
* Set ignored attributes for normalization and denormalization.
* Sets ignored attributes for normalization and denormalization.
*
* @deprecated since Symfony 4.2
*
* @return self
*/
public function setIgnoredAttributes(array $ignoredAttributes)
{
$this->ignoredAttributes = $ignoredAttributes;
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the "ignored_attributes" key of the context instead.', __METHOD__), \E_USER_DEPRECATED);
$this->defaultContext[self::IGNORED_ATTRIBUTES] = $this->ignoredAttributes = $ignoredAttributes;
return $this;
}
/**
* {@inheritdoc}
*/
public function hasCacheableSupportsMethod(): bool
{
return false;
}
/**
* Detects if the configured circular reference limit is reached.
*
@@ -152,16 +272,17 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
{
$objectHash = spl_object_hash($object);
if (isset($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash])) {
if ($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash] >= $this->circularReferenceLimit) {
unset($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash]);
$circularReferenceLimit = $context[self::CIRCULAR_REFERENCE_LIMIT] ?? $this->defaultContext[self::CIRCULAR_REFERENCE_LIMIT] ?? $this->circularReferenceLimit;
if (isset($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash])) {
if ($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash] >= $circularReferenceLimit) {
unset($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash]);
return true;
}
++$context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash];
++$context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash];
} else {
$context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash] = 1;
$context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash] = 1;
}
return false;
@@ -173,19 +294,30 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
* If a circular reference handler is set, it will be called. Otherwise, a
* {@class CircularReferenceException} will be thrown.
*
* @param object $object
* @final since Symfony 4.2
*
* @param object $object
* @param string|null $format
* @param array $context
*
* @return mixed
*
* @throws CircularReferenceException
*/
protected function handleCircularReference($object)
protected function handleCircularReference($object/* , string $format = null, array $context = [] */)
{
if ($this->circularReferenceHandler) {
return \call_user_func($this->circularReferenceHandler, $object);
if (\func_num_args() < 2 && __CLASS__ !== static::class && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface && !$this instanceof \Mockery\MockInterface) {
@trigger_error(sprintf('The "%s()" method will have two new "string $format = null" and "array $context = []" arguments in version 5.0, not defining it is deprecated since Symfony 4.2.', __METHOD__), \E_USER_DEPRECATED);
}
$format = \func_num_args() > 1 ? func_get_arg(1) : null;
$context = \func_num_args() > 2 ? func_get_arg(2) : [];
$circularReferenceHandler = $context[self::CIRCULAR_REFERENCE_HANDLER] ?? $this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER] ?? $this->circularReferenceHandler;
if ($circularReferenceHandler) {
return $circularReferenceHandler($object, $format, $context);
}
throw new CircularReferenceException(sprintf('A circular reference has been detected when serializing the object of class "%s" (configured limit: %d).', \get_class($object), $this->circularReferenceLimit));
throw new CircularReferenceException(sprintf('A circular reference has been detected when serializing the object of class "%s" (configured limit: %d).', \get_class($object), $context[self::CIRCULAR_REFERENCE_LIMIT] ?? $this->defaultContext[self::CIRCULAR_REFERENCE_LIMIT] ?? $this->circularReferenceLimit));
}
/**
@@ -200,18 +332,18 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
*/
protected function getAllowedAttributes($classOrObject, array $context, $attributesAsString = false)
{
$allowExtraAttributes = $context[self::ALLOW_EXTRA_ATTRIBUTES] ?? $this->defaultContext[self::ALLOW_EXTRA_ATTRIBUTES];
if (!$this->classMetadataFactory) {
if (isset($context[static::ALLOW_EXTRA_ATTRIBUTES]) && !$context[static::ALLOW_EXTRA_ATTRIBUTES]) {
throw new LogicException(sprintf('A class metadata factory must be provided in the constructor when setting "%s" to false.', static::ALLOW_EXTRA_ATTRIBUTES));
if (!$allowExtraAttributes) {
throw new LogicException(sprintf('A class metadata factory must be provided in the constructor when setting "%s" to false.', self::ALLOW_EXTRA_ATTRIBUTES));
}
return false;
}
$groups = false;
if (isset($context[static::GROUPS]) && \is_array($context[static::GROUPS])) {
$groups = $context[static::GROUPS];
} elseif (!isset($context[static::ALLOW_EXTRA_ATTRIBUTES]) || $context[static::ALLOW_EXTRA_ATTRIBUTES]) {
$tmpGroups = $context[self::GROUPS] ?? $this->defaultContext[self::GROUPS] ?? null;
$groups = (\is_array($tmpGroups) || \is_scalar($tmpGroups)) ? (array) $tmpGroups : false;
if (false === $groups && $allowExtraAttributes) {
return false;
}
@@ -241,17 +373,19 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
*/
protected function isAllowedAttribute($classOrObject, $attribute, $format = null, array $context = [])
{
if (\in_array($attribute, $this->ignoredAttributes)) {
$ignoredAttributes = $context[self::IGNORED_ATTRIBUTES] ?? $this->defaultContext[self::IGNORED_ATTRIBUTES] ?? $this->ignoredAttributes;
if (\in_array($attribute, $ignoredAttributes)) {
return false;
}
if (isset($context[self::ATTRIBUTES][$attribute])) {
$attributes = $context[self::ATTRIBUTES] ?? $this->defaultContext[self::ATTRIBUTES] ?? null;
if (isset($attributes[$attribute])) {
// Nested attributes
return true;
}
if (isset($context[self::ATTRIBUTES]) && \is_array($context[self::ATTRIBUTES])) {
return \in_array($attribute, $context[self::ATTRIBUTES], true);
if (\is_array($attributes)) {
return \in_array($attribute, $attributes, true);
}
return true;
@@ -298,24 +432,12 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
* @return object
*
* @throws RuntimeException
* @throws MissingConstructorArgumentsException
*/
protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes/*, string $format = null*/)
protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes, string $format = null)
{
if (\func_num_args() >= 6) {
$format = func_get_arg(5);
} else {
if (__CLASS__ !== static::class) {
$r = new \ReflectionMethod($this, __FUNCTION__);
if (__CLASS__ !== $r->getDeclaringClass()->getName()) {
@trigger_error(sprintf('Method %s::%s() will have a 6th `string $format = null` argument in version 4.0. Not defining it is deprecated since Symfony 3.2.', static::class, __FUNCTION__), \E_USER_DEPRECATED);
}
}
$format = null;
}
if (null !== $object = $this->extractObjectToPopulate($class, $context, static::OBJECT_TO_POPULATE)) {
unset($context[static::OBJECT_TO_POPULATE]);
if (null !== $object = $this->extractObjectToPopulate($class, $context, self::OBJECT_TO_POPULATE)) {
unset($context[self::OBJECT_TO_POPULATE]);
return $object;
}
@@ -333,11 +455,11 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
$params = [];
foreach ($constructorParameters as $constructorParameter) {
$paramName = $constructorParameter->name;
$key = $this->nameConverter ? $this->nameConverter->normalize($paramName) : $paramName;
$key = $this->nameConverter ? $this->nameConverter->normalize($paramName, $class, $format, $context) : $paramName;
$allowed = false === $allowedAttributes || \in_array($paramName, $allowedAttributes);
$ignored = !$this->isAllowedAttribute($class, $paramName, $format, $context);
if (method_exists($constructorParameter, 'isVariadic') && $constructorParameter->isVariadic()) {
if ($constructorParameter->isVariadic()) {
if ($allowed && !$ignored && (isset($data[$key]) || \array_key_exists($key, $data))) {
if (!\is_array($data[$paramName])) {
throw new RuntimeException(sprintf('Cannot create an instance of "%s" from serialized data because the variadic parameter "%s" can only accept an array.', $class, $constructorParameter->name));
@@ -363,10 +485,16 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
// Don't run set for a parameter passed to the constructor
$params[] = $this->denormalizeParameter($reflectionClass, $constructorParameter, $paramName, $parameterData, $context, $format);
unset($data[$key]);
} elseif (\array_key_exists($key, $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class] ?? [])) {
$params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
} elseif (\array_key_exists($key, $this->defaultContext[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class] ?? [])) {
$params[] = $this->defaultContext[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key];
} elseif ($constructorParameter->isDefaultValueAvailable()) {
$params[] = $constructorParameter->getDefaultValue();
} elseif ($constructorParameter->hasType() && $constructorParameter->getType()->allowsNull()) {
$params[] = null;
} else {
throw new RuntimeException(sprintf('Cannot create an instance of "%s" from serialized data because its constructor requires parameter "%s" to be present.', $class, $constructorParameter->name));
throw new MissingConstructorArgumentsException(sprintf('Cannot create an instance of "%s" from serialized data because its constructor requires parameter "%s" to be present.', $class, $constructorParameter->name));
}
}
@@ -386,38 +514,39 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
protected function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameter, $parameterName, $parameterData, array $context, $format = null)
{
try {
if (\PHP_VERSION_ID < 70100 && null !== $parameterClass = $parameter->getClass()) {
$parameterClass = $parameterClass->name;
} elseif (\PHP_VERSION_ID >= 70100 && ($parameterType = $parameter->getType()) instanceof \ReflectionNamedType && !$parameterType->isBuiltin()) {
if (($parameterType = $parameter->getType()) instanceof \ReflectionNamedType && !$parameterType->isBuiltin()) {
$parameterClass = $parameterType->getName();
new \ReflectionClass($parameterClass); // throws a \ReflectionException if the class doesn't exist
} else {
$parameterClass = null;
}
if (null !== $parameterClass) {
if (!$this->serializer instanceof DenormalizerInterface) {
throw new LogicException(sprintf('Cannot create an instance of "%s" from serialized data because the serializer inject in "%s" is not a denormalizer.', $parameterClass, static::class));
}
return $this->serializer->denormalize($parameterData, $parameterClass, $format, $this->createChildContext($context, $parameterName, $format));
$parameterData = $this->serializer->denormalize($parameterData, $parameterClass, $format, $this->createChildContext($context, $parameterName, $format));
}
return $parameterData;
} catch (\ReflectionException $e) {
throw new RuntimeException(sprintf('Could not determine the class of the parameter "%s".', $parameterName), 0, $e);
} catch (MissingConstructorArgumentsException $e) {
if (!$parameter->getType()->allowsNull()) {
throw $e;
}
return null;
}
return $this->applyCallbacks($parameterData, $class->getName(), $parameterName, $format, $context);
}
/**
* @param string $attribute Attribute name
*
* @return array
*
* @internal
*/
protected function createChildContext(array $parentContext, $attribute/*, string $format = null */)
protected function createChildContext(array $parentContext, $attribute/* , ?string $format */): array
{
if (\func_num_args() < 3) {
@trigger_error(sprintf('Method "%s::%s()" will have a third "?string $format" argument in version 5.0; not defining it is deprecated since Symfony 4.3.', static::class, __FUNCTION__), \E_USER_DEPRECATED);
}
if (isset($parentContext[self::ATTRIBUTES][$attribute])) {
$parentContext[self::ATTRIBUTES] = $parentContext[self::ATTRIBUTES][$attribute];
} else {
@@ -426,4 +555,46 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
return $parentContext;
}
/**
* Validate callbacks set in context.
*
* @param string $contextType Used to specify which context is invalid in exceptions
*
* @throws InvalidArgumentException
*/
final protected function validateCallbackContext(array $context, string $contextType = ''): void
{
if (!isset($context[self::CALLBACKS])) {
return;
}
if (!\is_array($context[self::CALLBACKS])) {
throw new InvalidArgumentException(sprintf('The "%s"%s context option must be an array of callables.', self::CALLBACKS, '' !== $contextType ? " $contextType" : ''));
}
foreach ($context[self::CALLBACKS] as $attribute => $callback) {
if (!\is_callable($callback)) {
throw new InvalidArgumentException(sprintf('Invalid callback found for attribute "%s" in the "%s"%s context option.', $attribute, self::CALLBACKS, '' !== $contextType ? " $contextType" : ''));
}
}
}
/**
* Apply callbacks set in context.
*
* @param mixed $value
* @param object|string $object Can be either the object being normalizing or the object's class being denormalized
*
* @return mixed
*/
final protected function applyCallbacks($value, $object, string $attribute, ?string $format, array $context)
{
/**
* @var callable|null
*/
$callback = $context[self::CALLBACKS][$attribute] ?? $this->defaultContext[self::CALLBACKS][$attribute] ?? $this->callbacks[$attribute] ?? null;
return $callback ? $callback($value, $object, $attribute, $format, $context) : $value;
}
}
@@ -11,14 +11,21 @@
namespace Symfony\Component\Serializer\Normalizer;
use Symfony\Component\PropertyAccess\Exception\AccessException;
use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException;
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Exception\ExtraAttributesException;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
use Symfony\Component\Serializer\Exception\RuntimeException;
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
@@ -29,19 +36,97 @@ use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
*/
abstract class AbstractObjectNormalizer extends AbstractNormalizer
{
const ENABLE_MAX_DEPTH = 'enable_max_depth';
const DEPTH_KEY_PATTERN = 'depth_%s::%s';
const DISABLE_TYPE_ENFORCEMENT = 'disable_type_enforcement';
/**
* Set to true to respect the max depth metadata on fields.
*/
public const ENABLE_MAX_DEPTH = 'enable_max_depth';
/**
* How to track the current depth in the context.
*/
public const DEPTH_KEY_PATTERN = 'depth_%s::%s';
/**
* While denormalizing, we can verify that types match.
*
* You can disable this by setting this flag to true.
*/
public const DISABLE_TYPE_ENFORCEMENT = 'disable_type_enforcement';
/**
* Flag to control whether fields with the value `null` should be output
* when normalizing or omitted.
*/
public const SKIP_NULL_VALUES = 'skip_null_values';
/**
* Callback to allow to set a value for an attribute when the max depth has
* been reached.
*
* If no callback is given, the attribute is skipped. If a callable is
* given, its return value is used (even if null).
*
* The arguments are:
*
* - mixed $attributeValue value of this field
* - object $object the whole object being normalized
* - string $attributeName name of the attribute being normalized
* - string $format the requested format
* - array $context the serialization context
*/
public const MAX_DEPTH_HANDLER = 'max_depth_handler';
/**
* Specify which context key are not relevant to determine which attributes
* of an object to (de)normalize.
*/
public const EXCLUDE_FROM_CACHE_KEY = 'exclude_from_cache_key';
/**
* Flag to tell the denormalizer to also populate existing objects on
* attributes of the main object.
*
* Setting this to true is only useful if you also specify the root object
* in OBJECT_TO_POPULATE.
*/
public const DEEP_OBJECT_TO_POPULATE = 'deep_object_to_populate';
public const PRESERVE_EMPTY_OBJECTS = 'preserve_empty_objects';
private $propertyTypeExtractor;
private $typesCache = [];
private $attributesCache = [];
private $cache = [];
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null)
/**
* @deprecated since Symfony 4.2
*
* @var callable|null
*/
private $maxDepthHandler;
private $objectClassResolver;
/**
* @var ClassDiscriminatorResolverInterface|null
*/
protected $classDiscriminatorResolver;
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = [])
{
parent::__construct($classMetadataFactory, $nameConverter);
parent::__construct($classMetadataFactory, $nameConverter, $defaultContext);
if (isset($this->defaultContext[self::MAX_DEPTH_HANDLER]) && !\is_callable($this->defaultContext[self::MAX_DEPTH_HANDLER])) {
throw new InvalidArgumentException(sprintf('The "%s" given in the default context is not callable.', self::MAX_DEPTH_HANDLER));
}
$this->defaultContext[self::EXCLUDE_FROM_CACHE_KEY] = array_merge($this->defaultContext[self::EXCLUDE_FROM_CACHE_KEY] ?? [], [self::CIRCULAR_REFERENCE_LIMIT_COUNTERS]);
$this->propertyTypeExtractor = $propertyTypeExtractor;
if (null === $classDiscriminatorResolver && null !== $classMetadataFactory) {
$classDiscriminatorResolver = new ClassDiscriminatorFromClassMetadata($classMetadataFactory);
}
$this->classDiscriminatorResolver = $classDiscriminatorResolver;
$this->objectClassResolver = $objectClassResolver;
}
/**
@@ -61,32 +146,61 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
$context['cache_key'] = $this->getCacheKey($format, $context);
}
$this->validateCallbackContext($context);
if ($this->isCircularReference($object, $context)) {
return $this->handleCircularReference($object);
return $this->handleCircularReference($object, $format, $context);
}
$data = [];
$stack = [];
$attributes = $this->getAttributes($object, $format, $context);
$class = \get_class($object);
$class = $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
$attributesMetadata = $this->classMetadataFactory ? $this->classMetadataFactory->getMetadataFor($class)->getAttributesMetadata() : null;
if (isset($context[self::MAX_DEPTH_HANDLER])) {
$maxDepthHandler = $context[self::MAX_DEPTH_HANDLER];
if (!\is_callable($maxDepthHandler)) {
throw new InvalidArgumentException(sprintf('The "%s" given in the context is not callable.', self::MAX_DEPTH_HANDLER));
}
} else {
// already validated in constructor resp by type declaration of setMaxDepthHandler
$maxDepthHandler = $this->defaultContext[self::MAX_DEPTH_HANDLER] ?? $this->maxDepthHandler;
}
foreach ($attributes as $attribute) {
if (null !== $attributesMetadata && $this->isMaxDepthReached($attributesMetadata, $class, $attribute, $context)) {
$maxDepthReached = false;
if (null !== $attributesMetadata && ($maxDepthReached = $this->isMaxDepthReached($attributesMetadata, $class, $attribute, $context)) && !$maxDepthHandler) {
continue;
}
$attributeValue = $this->getAttributeValue($object, $attribute, $format, $context);
if (isset($this->callbacks[$attribute])) {
$attributeValue = \call_user_func($this->callbacks[$attribute], $attributeValue);
try {
$attributeValue = $this->getAttributeValue($object, $attribute, $format, $context);
} catch (AccessException $e) {
if (sprintf('The property "%s::$%s" is not initialized.', \get_class($object), $attribute) === $e->getMessage()) {
continue;
}
if (($p = $e->getPrevious()) && 'Error' === \get_class($p) && $this->isUninitializedValueError($p)) {
continue;
}
throw $e;
} catch (\Error $e) {
if ($this->isUninitializedValueError($e)) {
continue;
}
throw $e;
}
if (null !== $attributeValue && !is_scalar($attributeValue)) {
if ($maxDepthReached) {
$attributeValue = $maxDepthHandler($attributeValue, $object, $attribute, $format, $context);
}
$attributeValue = $this->applyCallbacks($attributeValue, $object, $attribute, $format, $context);
if (null !== $attributeValue && !\is_scalar($attributeValue)) {
$stack[$attribute] = $attributeValue;
}
$data = $this->updateData($data, $attribute, $attributeValue);
$data = $this->updateData($data, $attribute, $attributeValue, $class, $format, $context);
}
foreach ($stack as $attribute => $attributeValue) {
@@ -94,12 +208,39 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
throw new LogicException(sprintf('Cannot normalize attribute "%s" because the injected serializer is not a normalizer.', $attribute));
}
$data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $this->createChildContext($context, $attribute, $format)));
$data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $this->createChildContext($context, $attribute, $format)), $class, $format, $context);
}
if (isset($context[self::PRESERVE_EMPTY_OBJECTS]) && !\count($data)) {
return new \ArrayObject();
}
return $data;
}
/**
* {@inheritdoc}
*/
protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes, string $format = null)
{
if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForClass($class)) {
if (!isset($data[$mapping->getTypeProperty()])) {
throw new RuntimeException(sprintf('Type property "%s" not found for the abstract object "%s".', $mapping->getTypeProperty(), $class));
}
$type = $data[$mapping->getTypeProperty()];
if (null === ($mappedClass = $mapping->getClassForType($type))) {
throw new RuntimeException(sprintf('The type "%s" has no mapped class for the abstract object "%s".', $type, $class));
}
if ($mappedClass !== $class) {
return $this->instantiateObject($data, $mappedClass, $context, new \ReflectionClass($mappedClass), $allowedAttributes, $format);
}
}
return parent::instantiateObject($data, $class, $context, $reflectionClass, $allowedAttributes, $format);
}
/**
* Gets and caches attributes for the given object, format and context.
*
@@ -110,7 +251,7 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
*/
protected function getAttributes($object, $format, array $context)
{
$class = \get_class($object);
$class = $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
$key = $class.'-'.$context['cache_key'];
if (isset($this->attributesCache[$key])) {
@@ -129,6 +270,10 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
$attributes = $this->extractAttributes($object, $format, $context);
if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForMappedObject($object)) {
array_unshift($attributes, $mapping->getTypeProperty());
}
if ($context['cache_key']) {
$this->attributesCache[$key] = $attributes;
}
@@ -157,12 +302,24 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
*/
abstract protected function getAttributeValue($object, $attribute, $format = null, array $context = []);
/**
* Sets a handler function that will be called when the max depth is reached.
*
* @deprecated since Symfony 4.2
*/
public function setMaxDepthHandler(?callable $handler): void
{
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the "max_depth_handler" key of the context instead.', __METHOD__), \E_USER_DEPRECATED);
$this->maxDepthHandler = $handler;
}
/**
* {@inheritdoc}
*/
public function supportsDenormalization($data, $type, $format = null)
{
return isset($this->cache[$type]) ? $this->cache[$type] : $this->cache[$type] = class_exists($type);
return class_exists($type) || (interface_exists($type, false) && $this->classDiscriminatorResolver && null !== $this->classDiscriminatorResolver->getMappingForClass($type));
}
/**
@@ -174,35 +331,47 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
$context['cache_key'] = $this->getCacheKey($format, $context);
}
$this->validateCallbackContext($context);
$allowedAttributes = $this->getAllowedAttributes($type, $context, true);
$normalizedData = $this->prepareForDenormalization($data);
$extraAttributes = [];
$reflectionClass = new \ReflectionClass($type);
$object = $this->instantiateObject($normalizedData, $type, $context, $reflectionClass, $allowedAttributes, $format);
$resolvedClass = $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
foreach ($normalizedData as $attribute => $value) {
if ($this->nameConverter) {
$attribute = $this->nameConverter->denormalize($attribute);
$attribute = $this->nameConverter->denormalize($attribute, $resolvedClass, $format, $context);
}
if ((false !== $allowedAttributes && !\in_array($attribute, $allowedAttributes)) || !$this->isAllowedAttribute($type, $attribute, $format, $context)) {
if (isset($context[self::ALLOW_EXTRA_ATTRIBUTES]) && !$context[self::ALLOW_EXTRA_ATTRIBUTES]) {
if ((false !== $allowedAttributes && !\in_array($attribute, $allowedAttributes)) || !$this->isAllowedAttribute($resolvedClass, $attribute, $format, $context)) {
if (!($context[self::ALLOW_EXTRA_ATTRIBUTES] ?? $this->defaultContext[self::ALLOW_EXTRA_ATTRIBUTES])) {
$extraAttributes[] = $attribute;
}
continue;
}
$value = $this->validateAndDenormalize($type, $attribute, $value, $format, $context);
if ($context[self::DEEP_OBJECT_TO_POPULATE] ?? $this->defaultContext[self::DEEP_OBJECT_TO_POPULATE] ?? false) {
try {
$context[self::OBJECT_TO_POPULATE] = $this->getAttributeValue($object, $attribute, $format, $context);
} catch (NoSuchPropertyException $e) {
}
}
$value = $this->validateAndDenormalize($resolvedClass, $attribute, $value, $format, $context);
$value = $this->applyCallbacks($value, $resolvedClass, $attribute, $format, $context);
try {
$this->setAttributeValue($object, $attribute, $value, $format, $context);
} catch (InvalidArgumentException $e) {
throw new NotNormalizableValueException($e->getMessage(), $e->getCode(), $e);
throw new NotNormalizableValueException(sprintf('Failed to denormalize attribute "%s" value for class "%s": '.$e->getMessage(), $attribute, $type), $e->getCode(), $e);
}
}
if (!empty($extraAttributes)) {
if ($extraAttributes) {
throw new ExtraAttributesException($extraAttributes);
}
@@ -222,23 +391,25 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
/**
* Validates the submitted data and denormalizes it.
*
* @param string $currentClass
* @param string $attribute
* @param mixed $data
* @param string|null $format
* @param mixed $data
*
* @return mixed
*
* @throws NotNormalizableValueException
* @throws ExtraAttributesException
* @throws MissingConstructorArgumentsException
* @throws LogicException
*/
private function validateAndDenormalize($currentClass, $attribute, $data, $format, array $context)
private function validateAndDenormalize(string $currentClass, string $attribute, $data, ?string $format, array $context)
{
if (null === $this->propertyTypeExtractor || null === $types = $this->propertyTypeExtractor->getTypes($currentClass, $attribute)) {
if (null === $types = $this->getTypes($currentClass, $attribute)) {
return $data;
}
$expectedTypes = [];
$isUnionType = \count($types) > 1;
$extraAttributesException = null;
$missingConstructorArgumentException = null;
foreach ($types as $type) {
if (null === $data && $type->isNullable()) {
return null;
@@ -252,6 +423,10 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
$data = [$data];
}
if (XmlEncoder::FORMAT === $format && '' === $data && Type::BUILTIN_TYPE_ARRAY === $type->getBuiltinType()) {
return [];
}
if (null !== $collectionValueType && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) {
$builtinType = Type::BUILTIN_TYPE_OBJECT;
$class = $collectionValueType->getClassName().'[]';
@@ -259,6 +434,26 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
if (null !== $collectionKeyType = $type->getCollectionKeyType()) {
$context['key_type'] = $collectionKeyType;
}
} elseif ($type->isCollection() && null !== ($collectionValueType = $type->getCollectionValueType()) && Type::BUILTIN_TYPE_ARRAY === $collectionValueType->getBuiltinType()) {
// get inner type for any nested array
$innerType = $collectionValueType;
// note that it will break for any other builtinType
$dimensions = '[]';
while (null !== $innerType->getCollectionValueType() && Type::BUILTIN_TYPE_ARRAY === $innerType->getBuiltinType()) {
$dimensions .= '[]';
$innerType = $innerType->getCollectionValueType();
}
if (null !== $innerType->getClassName()) {
// the builtinType is the inner one and the class is the class followed by []...[]
$builtinType = $innerType->getBuiltinType();
$class = $innerType->getClassName().$dimensions;
} else {
// default fallback (keep it as array)
$builtinType = $type->getBuiltinType();
$class = $type->getClassName();
}
} else {
$builtinType = $type->getBuiltinType();
$class = $type->getClassName();
@@ -266,33 +461,72 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
$expectedTypes[Type::BUILTIN_TYPE_OBJECT === $builtinType && $class ? $class : $builtinType] = true;
if (Type::BUILTIN_TYPE_OBJECT === $builtinType) {
if (!$this->serializer instanceof DenormalizerInterface) {
throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer.', $attribute, $class));
// This try-catch should cover all NotNormalizableValueException (and all return branches after the first
// exception) so we could try denormalizing all types of an union type. If the target type is not an union
// type, we will just re-throw the catched exception.
// In the case of no denormalization succeeds with an union type, it will fall back to the default exception
// with the acceptable types list.
try {
if (Type::BUILTIN_TYPE_OBJECT === $builtinType) {
if (!$this->serializer instanceof DenormalizerInterface) {
throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer.', $attribute, $class));
}
$childContext = $this->createChildContext($context, $attribute, $format);
if ($this->serializer->supportsDenormalization($data, $class, $format, $childContext)) {
return $this->serializer->denormalize($data, $class, $format, $childContext);
}
}
$childContext = $this->createChildContext($context, $attribute, $format);
if ($this->serializer->supportsDenormalization($data, $class, $format, $childContext)) {
return $this->serializer->denormalize($data, $class, $format, $childContext);
// JSON only has a Number type corresponding to both int and float PHP types.
// PHP's json_encode, JavaScript's JSON.stringify, Go's json.Marshal as well as most other JSON encoders convert
// floating-point numbers like 12.0 to 12 (the decimal part is dropped when possible).
// PHP's json_decode automatically converts Numbers without a decimal part to integers.
// To circumvent this behavior, integers are converted to floats when denormalizing JSON based formats and when
// a float is expected.
if (Type::BUILTIN_TYPE_FLOAT === $builtinType && \is_int($data) && null !== $format && str_contains($format, JsonEncoder::FORMAT)) {
return (float) $data;
}
}
// JSON only has a Number type corresponding to both int and float PHP types.
// PHP's json_encode, JavaScript's JSON.stringify, Go's json.Marshal as well as most other JSON encoders convert
// floating-point numbers like 12.0 to 12 (the decimal part is dropped when possible).
// PHP's json_decode automatically converts Numbers without a decimal part to integers.
// To circumvent this behavior, integers are converted to floats when denormalizing JSON based formats and when
// a float is expected.
if (Type::BUILTIN_TYPE_FLOAT === $builtinType && \is_int($data) && false !== strpos($format, JsonEncoder::FORMAT)) {
return (float) $data;
}
if ('false' === $builtinType && false === $data) {
return $data;
}
if (\call_user_func('is_'.$builtinType, $data)) {
return $data;
if (('is_'.$builtinType)($data)) {
return $data;
}
} catch (NotNormalizableValueException $e) {
if (!$isUnionType) {
throw $e;
}
} catch (ExtraAttributesException $e) {
if (!$isUnionType) {
throw $e;
}
if (!$extraAttributesException) {
$extraAttributesException = $e;
}
} catch (MissingConstructorArgumentsException $e) {
if (!$isUnionType) {
throw $e;
}
if (!$missingConstructorArgumentException) {
$missingConstructorArgumentException = $e;
}
}
}
if (!empty($context[self::DISABLE_TYPE_ENFORCEMENT])) {
if ($extraAttributesException) {
throw $extraAttributesException;
}
if ($missingConstructorArgumentException) {
throw $missingConstructorArgumentException;
}
if ($context[self::DISABLE_TYPE_ENFORCEMENT] ?? $this->defaultContext[self::DISABLE_TYPE_ENFORCEMENT] ?? false) {
return $data;
}
@@ -304,25 +538,65 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
*/
protected function denormalizeParameter(\ReflectionClass $class, \ReflectionParameter $parameter, $parameterName, $parameterData, array $context, $format = null)
{
if ((method_exists($parameter, 'isVariadic') && $parameter->isVariadic()) || null === $this->propertyTypeExtractor || null === $this->propertyTypeExtractor->getTypes($class->getName(), $parameterName)) {
if ($parameter->isVariadic() || null === $this->propertyTypeExtractor || null === $this->propertyTypeExtractor->getTypes($class->getName(), $parameterName)) {
return parent::denormalizeParameter($class, $parameter, $parameterName, $parameterData, $context, $format);
}
return $this->validateAndDenormalize($class->getName(), $parameterName, $parameterData, $format, $context);
$parameterData = $this->validateAndDenormalize($class->getName(), $parameterName, $parameterData, $format, $context);
return $this->applyCallbacks($parameterData, $class->getName(), $parameterName, $format, $context);
}
/**
* @return Type[]|null
*/
private function getTypes(string $currentClass, string $attribute): ?array
{
if (null === $this->propertyTypeExtractor) {
return null;
}
$key = $currentClass.'::'.$attribute;
if (isset($this->typesCache[$key])) {
return false === $this->typesCache[$key] ? null : $this->typesCache[$key];
}
if (null !== $types = $this->propertyTypeExtractor->getTypes($currentClass, $attribute)) {
return $this->typesCache[$key] = $types;
}
if (null !== $this->classDiscriminatorResolver && null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForClass($currentClass)) {
if ($discriminatorMapping->getTypeProperty() === $attribute) {
return $this->typesCache[$key] = [
new Type(Type::BUILTIN_TYPE_STRING),
];
}
foreach ($discriminatorMapping->getTypesMapping() as $mappedClass) {
if (null !== $types = $this->propertyTypeExtractor->getTypes($mappedClass, $attribute)) {
return $this->typesCache[$key] = $types;
}
}
}
$this->typesCache[$key] = false;
return null;
}
/**
* Sets an attribute and apply the name converter if necessary.
*
* @param string $attribute
* @param mixed $attributeValue
*
* @return array
* @param mixed $attributeValue
*/
private function updateData(array $data, $attribute, $attributeValue)
private function updateData(array $data, string $attribute, $attributeValue, string $class, ?string $format, array $context): array
{
if (null === $attributeValue && ($context[self::SKIP_NULL_VALUES] ?? $this->defaultContext[self::SKIP_NULL_VALUES] ?? false)) {
return $data;
}
if ($this->nameConverter) {
$attribute = $this->nameConverter->normalize($attribute);
$attribute = $this->nameConverter->normalize($attribute, $class, $format, $context);
}
$data[$attribute] = $attributeValue;
@@ -334,23 +608,19 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
* Is the max depth reached for the given attribute?
*
* @param AttributeMetadataInterface[] $attributesMetadata
* @param string $class
* @param string $attribute
*
* @return bool
*/
private function isMaxDepthReached(array $attributesMetadata, $class, $attribute, array &$context)
private function isMaxDepthReached(array $attributesMetadata, string $class, string $attribute, array &$context): bool
{
$enableMaxDepth = $context[self::ENABLE_MAX_DEPTH] ?? $this->defaultContext[self::ENABLE_MAX_DEPTH] ?? false;
if (
!isset($context[static::ENABLE_MAX_DEPTH]) ||
!$context[static::ENABLE_MAX_DEPTH] ||
!$enableMaxDepth ||
!isset($attributesMetadata[$attribute]) ||
null === $maxDepth = $attributesMetadata[$attribute]->getMaxDepth()
) {
return false;
}
$key = sprintf(static::DEPTH_KEY_PATTERN, $class, $attribute);
$key = sprintf(self::DEPTH_KEY_PATTERN, $class, $attribute);
if (!isset($context[$key])) {
$context[$key] = 1;
@@ -372,18 +642,21 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
* We must not mix up the attribute cache between parent and children.
*
* {@inheritdoc}
*
* @param string|null $format
*
* @internal
*/
protected function createChildContext(array $parentContext, $attribute/*, string $format = null */)
protected function createChildContext(array $parentContext, $attribute/* , ?string $format */): array
{
if (\func_num_args() >= 3) {
$format = func_get_arg(2);
} else {
// will be deprecated in version 4
@trigger_error(sprintf('Method "%s::%s()" will have a third "?string $format" argument in version 5.0; not defining it is deprecated since Symfony 4.3.', static::class, __FUNCTION__), \E_USER_DEPRECATED);
$format = null;
}
$context = parent::createChildContext($parentContext, $attribute, $format);
// format is already included in the cache_key of the parent.
$context['cache_key'] = $this->getCacheKey($format, $context);
return $context;
@@ -394,23 +667,37 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
*
* The key must be different for every option in the context that could change which attributes should be handled.
*
* @param string|null $format
*
* @return bool|string
*/
private function getCacheKey($format, array $context)
private function getCacheKey(?string $format, array $context)
{
foreach ($context[self::EXCLUDE_FROM_CACHE_KEY] ?? $this->defaultContext[self::EXCLUDE_FROM_CACHE_KEY] as $key) {
unset($context[$key]);
}
unset($context[self::EXCLUDE_FROM_CACHE_KEY]);
unset($context[self::OBJECT_TO_POPULATE]);
unset($context['cache_key']); // avoid artificially different keys
try {
return md5($format.serialize([
'context' => $context,
'ignored' => $this->ignoredAttributes,
'camelized' => $this->camelizedAttributes,
]));
} catch (\Exception $exception) {
} catch (\Exception $e) {
// The context cannot be serialized, skip the cache
return false;
}
}
/**
* This error may occur when specific object normalizer implementation gets attribute value
* by accessing a public uninitialized property or by calling a method accessing such property.
*/
private function isUninitializedValueError(\Error $e): bool
{
return \PHP_VERSION_ID >= 70400
&& str_starts_with($e->getMessage(), 'Typed property')
&& str_ends_with($e->getMessage(), 'must not be accessed before initialization');
}
}
+16 -8
View File
@@ -22,9 +22,9 @@ use Symfony\Component\Serializer\SerializerInterface;
*
* @author Alexander M. Turek <me@derrabus.de>
*
* @final since version 3.3.
* @final
*/
class ArrayDenormalizer implements DenormalizerInterface, SerializerAwareInterface
class ArrayDenormalizer implements ContextAwareDenormalizerInterface, SerializerAwareInterface, CacheableSupportsMethodInterface
{
/**
* @var SerializerInterface|DenormalizerInterface
@@ -35,6 +35,8 @@ class ArrayDenormalizer implements DenormalizerInterface, SerializerAwareInterfa
* {@inheritdoc}
*
* @throws NotNormalizableValueException
*
* @return array
*/
public function denormalize($data, $type, $format = null, array $context = [])
{
@@ -44,7 +46,7 @@ class ArrayDenormalizer implements DenormalizerInterface, SerializerAwareInterfa
if (!\is_array($data)) {
throw new InvalidArgumentException('Data expected to be an array, '.\gettype($data).' given.');
}
if ('[]' !== substr($type, -2)) {
if (!str_ends_with($type, '[]')) {
throw new InvalidArgumentException('Unsupported class: '.$type);
}
@@ -53,7 +55,7 @@ class ArrayDenormalizer implements DenormalizerInterface, SerializerAwareInterfa
$builtinType = isset($context['key_type']) ? $context['key_type']->getBuiltinType() : null;
foreach ($data as $key => $value) {
if (null !== $builtinType && !\call_user_func('is_'.$builtinType, $key)) {
if (null !== $builtinType && !('is_'.$builtinType)($key)) {
throw new NotNormalizableValueException(sprintf('The type of the key "%s" must be "%s" ("%s" given).', $key, $builtinType, \gettype($key)));
}
@@ -66,15 +68,13 @@ class ArrayDenormalizer implements DenormalizerInterface, SerializerAwareInterfa
/**
* {@inheritdoc}
*/
public function supportsDenormalization($data, $type, $format = null/*, array $context = []*/)
public function supportsDenormalization($data, $type, $format = null, array $context = []): bool
{
if (null === $this->serializer) {
throw new BadMethodCallException(sprintf('The serializer needs to be set to allow "%s()" to be used.', __METHOD__));
}
$context = \func_num_args() > 3 ? func_get_arg(3) : [];
return '[]' === substr($type, -2)
return str_ends_with($type, '[]')
&& $this->serializer->supportsDenormalization($data, substr($type, 0, -2), $format, $context);
}
@@ -89,4 +89,12 @@ class ArrayDenormalizer implements DenormalizerInterface, SerializerAwareInterfa
$this->serializer = $serializer;
}
/**
* {@inheritdoc}
*/
public function hasCacheableSupportsMethod(): bool
{
return $this->serializer instanceof CacheableSupportsMethodInterface && $this->serializer->hasCacheableSupportsMethod();
}
}
+10 -12
View File
@@ -17,13 +17,11 @@ use Symfony\Component\Serializer\SerializerAwareTrait;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class CustomNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
class CustomNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface, CacheableSupportsMethodInterface
{
use ObjectToPopulateTrait;
use SerializerAwareTrait;
private $cache = [];
/**
* {@inheritdoc}
*/
@@ -37,7 +35,7 @@ class CustomNormalizer implements NormalizerInterface, DenormalizerInterface, Se
*/
public function denormalize($data, $type, $format = null, array $context = [])
{
$object = $this->extractObjectToPopulate($type, $context) ?: new $type();
$object = $this->extractObjectToPopulate($type, $context) ?? new $type();
$object->denormalize($this->serializer, $data, $format, $context);
return $object;
@@ -67,14 +65,14 @@ class CustomNormalizer implements NormalizerInterface, DenormalizerInterface, Se
*/
public function supportsDenormalization($data, $type, $format = null)
{
if (isset($this->cache[$type])) {
return $this->cache[$type];
}
return is_subclass_of($type, DenormalizableInterface::class);
}
if (!class_exists($type)) {
return $this->cache[$type] = false;
}
return $this->cache[$type] = is_subclass_of($type, 'Symfony\Component\Serializer\Normalizer\DenormalizableInterface');
/**
* {@inheritdoc}
*/
public function hasCacheableSupportsMethod(): bool
{
return __CLASS__ === static::class;
}
}
+42 -16
View File
@@ -13,7 +13,9 @@ namespace Symfony\Component\Serializer\Normalizer;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesser;
use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface;
use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface as DeprecatedMimeTypeGuesserInterface;
use Symfony\Component\Mime\MimeTypeGuesserInterface;
use Symfony\Component\Mime\MimeTypes;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
@@ -23,9 +25,9 @@ use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class DataUriNormalizer implements NormalizerInterface, DenormalizerInterface
class DataUriNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface
{
private static $supportedTypes = [
private const SUPPORTED_TYPES = [
\SplFileInfo::class => true,
\SplFileObject::class => true,
File::class => true,
@@ -36,10 +38,22 @@ class DataUriNormalizer implements NormalizerInterface, DenormalizerInterface
*/
private $mimeTypeGuesser;
public function __construct(MimeTypeGuesserInterface $mimeTypeGuesser = null)
/**
* @param MimeTypeGuesserInterface|null $mimeTypeGuesser
*/
public function __construct($mimeTypeGuesser = null)
{
if (null === $mimeTypeGuesser && class_exists('Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesser')) {
$mimeTypeGuesser = MimeTypeGuesser::getInstance();
if ($mimeTypeGuesser instanceof DeprecatedMimeTypeGuesserInterface) {
@trigger_error(sprintf('Passing a %s to "%s()" is deprecated since Symfony 4.3, pass a "%s" instead.', DeprecatedMimeTypeGuesserInterface::class, __METHOD__, MimeTypeGuesserInterface::class), \E_USER_DEPRECATED);
} elseif (null === $mimeTypeGuesser) {
if (class_exists(MimeTypes::class)) {
$mimeTypeGuesser = MimeTypes::getDefault();
} elseif (class_exists(MimeTypeGuesser::class)) {
@trigger_error(sprintf('Passing null to "%s()" to use a default MIME type guesser without Symfony Mime installed is deprecated since Symfony 4.3. Try running "composer require symfony/mime".', __METHOD__), \E_USER_DEPRECATED);
$mimeTypeGuesser = MimeTypeGuesser::getInstance();
}
} elseif (!$mimeTypeGuesser instanceof MimeTypes) {
throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be an instance of "%s" or null, "%s" given.', __METHOD__, MimeTypes::class, \is_object($mimeTypeGuesser) ? \get_class($mimeTypeGuesser) : \gettype($mimeTypeGuesser)));
}
$this->mimeTypeGuesser = $mimeTypeGuesser;
@@ -47,6 +61,8 @@ class DataUriNormalizer implements NormalizerInterface, DenormalizerInterface
/**
* {@inheritdoc}
*
* @return string
*/
public function normalize($object, $format = null, array $context = [])
{
@@ -88,16 +104,18 @@ class DataUriNormalizer implements NormalizerInterface, DenormalizerInterface
*
* @throws InvalidArgumentException
* @throws NotNormalizableValueException
*
* @return \SplFileInfo
*/
public function denormalize($data, $type, $format = null, array $context = [])
{
if (!preg_match('/^data:([a-z0-9][a-z0-9\!\#\$\&\-\^\_\+\.]{0,126}\/[a-z0-9][a-z0-9\!\#\$\&\-\^\_\+\.]{0,126}(;[a-z0-9\-]+\=[a-z0-9\-]+)?)?(;base64)?,[a-z0-9\!\$\&\\\'\,\(\)\*\+\,\;\=\-\.\_\~\:\@\/\?\%\s]*\s*$/i', $data)) {
if (null === $data || !preg_match('/^data:([a-z0-9][a-z0-9\!\#\$\&\-\^\_\+\.]{0,126}\/[a-z0-9][a-z0-9\!\#\$\&\-\^\_\+\.]{0,126}(;[a-z0-9\-]+\=[a-z0-9\-]+)?)?(;base64)?,[a-z0-9\!\$\&\\\'\,\(\)\*\+\,\;\=\-\.\_\~\:\@\/\?\%\s]*\s*$/i', $data)) {
throw new NotNormalizableValueException('The provided "data:" URI is not valid.');
}
try {
switch ($type) {
case 'Symfony\Component\HttpFoundation\File\File':
case File::class:
return new File($data, false);
case 'SplFileObject':
@@ -116,21 +134,31 @@ class DataUriNormalizer implements NormalizerInterface, DenormalizerInterface
*/
public function supportsDenormalization($data, $type, $format = null)
{
return isset(self::$supportedTypes[$type]);
return isset(self::SUPPORTED_TYPES[$type]);
}
/**
* {@inheritdoc}
*/
public function hasCacheableSupportsMethod(): bool
{
return __CLASS__ === static::class;
}
/**
* Gets the mime type of the object. Defaults to application/octet-stream.
*
* @return string
*/
private function getMimeType(\SplFileInfo $object)
private function getMimeType(\SplFileInfo $object): string
{
if ($object instanceof File) {
return $object->getMimeType();
}
if ($this->mimeTypeGuesser && $mimeType = $this->mimeTypeGuesser->guess($object->getPathname())) {
if ($this->mimeTypeGuesser instanceof DeprecatedMimeTypeGuesserInterface && $mimeType = $this->mimeTypeGuesser->guess($object->getPathname())) {
return $mimeType;
}
if ($this->mimeTypeGuesser && $mimeType = $this->mimeTypeGuesser->guessMimeType($object->getPathname())) {
return $mimeType;
}
@@ -139,10 +167,8 @@ class DataUriNormalizer implements NormalizerInterface, DenormalizerInterface
/**
* Returns the \SplFileObject instance associated with the given \SplFileInfo instance.
*
* @return \SplFileObject
*/
private function extractSplFileObject(\SplFileInfo $object)
private function extractSplFileObject(\SplFileInfo $object): \SplFileObject
{
if ($object instanceof \SplFileObject) {
return $object;
@@ -20,24 +20,34 @@ use Symfony\Component\Serializer\Exception\UnexpectedValueException;
*
* @author Jérôme Parmentier <jerome@prmntr.me>
*/
class DateIntervalNormalizer implements NormalizerInterface, DenormalizerInterface
class DateIntervalNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface
{
const FORMAT_KEY = 'dateinterval_format';
public const FORMAT_KEY = 'dateinterval_format';
private $format;
private $defaultContext = [
self::FORMAT_KEY => '%rP%yY%mM%dDT%hH%iM%sS',
];
/**
* @param string $format
* @param array $defaultContext
*/
public function __construct($format = '%rP%yY%mM%dDT%hH%iM%sS')
public function __construct($defaultContext = [])
{
$this->format = $format;
if (!\is_array($defaultContext)) {
@trigger_error(sprintf('The "format" parameter is deprecated since Symfony 4.2, use the "%s" key of the context instead.', self::FORMAT_KEY), \E_USER_DEPRECATED);
$defaultContext = [self::FORMAT_KEY => (string) $defaultContext];
}
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
}
/**
* {@inheritdoc}
*
* @throws InvalidArgumentException
*
* @return string
*/
public function normalize($object, $format = null, array $context = [])
{
@@ -45,9 +55,7 @@ class DateIntervalNormalizer implements NormalizerInterface, DenormalizerInterfa
throw new InvalidArgumentException('The object must be an instance of "\DateInterval".');
}
$dateIntervalFormat = isset($context[self::FORMAT_KEY]) ? $context[self::FORMAT_KEY] : $this->format;
return $object->format($dateIntervalFormat);
return $object->format($context[self::FORMAT_KEY] ?? $this->defaultContext[self::FORMAT_KEY]);
}
/**
@@ -58,11 +66,21 @@ class DateIntervalNormalizer implements NormalizerInterface, DenormalizerInterfa
return $data instanceof \DateInterval;
}
/**
* {@inheritdoc}
*/
public function hasCacheableSupportsMethod(): bool
{
return __CLASS__ === static::class;
}
/**
* {@inheritdoc}
*
* @throws InvalidArgumentException
* @throws UnexpectedValueException
*
* @return \DateInterval
*/
public function denormalize($data, $type, $format = null, array $context = [])
{
@@ -74,7 +92,7 @@ class DateIntervalNormalizer implements NormalizerInterface, DenormalizerInterfa
throw new UnexpectedValueException('Expected a valid ISO 8601 interval string.');
}
$dateIntervalFormat = isset($context[self::FORMAT_KEY]) ? $context[self::FORMAT_KEY] : $this->format;
$dateIntervalFormat = $context[self::FORMAT_KEY] ?? $this->defaultContext[self::FORMAT_KEY];
$signPattern = '';
switch (substr($dateIntervalFormat, 0, 2)) {
@@ -87,7 +105,7 @@ class DateIntervalNormalizer implements NormalizerInterface, DenormalizerInterfa
$dateIntervalFormat = substr($dateIntervalFormat, 2);
break;
}
$valuePattern = '/^'.$signPattern.preg_replace('/%([yYmMdDhHiIsSwW])(\w)/', '(?P<$1>\d+)$2', $dateIntervalFormat).'$/';
$valuePattern = '/^'.$signPattern.preg_replace('/%([yYmMdDhHiIsSwW])(\w)/', '(?:(?P<$1>\d+)$2)?', preg_replace('/(T.*)$/', '($1)?', $dateIntervalFormat)).'$/';
if (!preg_match($valuePattern, $data)) {
throw new UnexpectedValueException(sprintf('Value "%s" contains intervals not accepted by format "%s".', $data, $dateIntervalFormat));
}
@@ -118,7 +136,7 @@ class DateIntervalNormalizer implements NormalizerInterface, DenormalizerInterfa
return \DateInterval::class === $type;
}
private function isISO8601($string)
private function isISO8601(string $string): bool
{
return preg_match('/^[\-+]?P(?=\w*(?:\d|%\w))(?:\d+Y|%[yY]Y)?(?:\d+M|%[mM]M)?(?:(?:\d+D|%[dD]D)|(?:\d+W|%[wW]W))?(?:T(?:\d+H|[hH]H)?(?:\d+M|[iI]M)?(?:\d+S|[sS]S)?)?$/', $string);
}
+51 -24
View File
@@ -20,33 +20,45 @@ use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface
class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface
{
const FORMAT_KEY = 'datetime_format';
const TIMEZONE_KEY = 'datetime_timezone';
public const FORMAT_KEY = 'datetime_format';
public const TIMEZONE_KEY = 'datetime_timezone';
private $format;
private $timezone;
private $defaultContext;
private static $supportedTypes = [
private const SUPPORTED_TYPES = [
\DateTimeInterface::class => true,
\DateTimeImmutable::class => true,
\DateTime::class => true,
];
/**
* @param string $format
* @param array $defaultContext
*/
public function __construct($format = \DateTime::RFC3339, \DateTimeZone $timezone = null)
public function __construct($defaultContext = [], \DateTimeZone $timezone = null)
{
$this->format = $format;
$this->timezone = $timezone;
$this->defaultContext = [
self::FORMAT_KEY => \DateTime::RFC3339,
self::TIMEZONE_KEY => null,
];
if (!\is_array($defaultContext)) {
@trigger_error('Passing configuration options directly to the constructor is deprecated since Symfony 4.2, use the default context instead.', \E_USER_DEPRECATED);
$defaultContext = [self::FORMAT_KEY => (string) $defaultContext];
$defaultContext[self::TIMEZONE_KEY] = $timezone;
}
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
}
/**
* {@inheritdoc}
*
* @throws InvalidArgumentException
*
* @return string
*/
public function normalize($object, $format = null, array $context = [])
{
@@ -54,7 +66,7 @@ class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface
throw new InvalidArgumentException('The object must implement the "\DateTimeInterface".');
}
$format = isset($context[self::FORMAT_KEY]) ? $context[self::FORMAT_KEY] : $this->format;
$dateTimeFormat = $context[self::FORMAT_KEY] ?? $this->defaultContext[self::FORMAT_KEY];
$timezone = $this->getTimezone($context);
if (null !== $timezone) {
@@ -62,7 +74,7 @@ class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface
$object = $object->setTimezone($timezone);
}
return $object->format($format);
return $object->format($dateTimeFormat);
}
/**
@@ -77,23 +89,20 @@ class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface
* {@inheritdoc}
*
* @throws NotNormalizableValueException
*
* @return \DateTimeInterface
*/
public function denormalize($data, $type, $format = null, array $context = [])
{
$dateTimeFormat = isset($context[self::FORMAT_KEY]) ? $context[self::FORMAT_KEY] : null;
$dateTimeFormat = $context[self::FORMAT_KEY] ?? null;
$timezone = $this->getTimezone($context);
if ('' === $data || null === $data) {
if (null === $data || (\is_string($data) && '' === trim($data))) {
throw new NotNormalizableValueException('The data is either an empty string or null, you should pass a string that can be parsed with the passed format or a valid DateTime string.');
}
if (null !== $dateTimeFormat) {
if (null === $timezone && \PHP_VERSION_ID < 70000) {
// https://bugs.php.net/68669
$object = \DateTime::class === $type ? \DateTime::createFromFormat($dateTimeFormat, $data) : \DateTimeImmutable::createFromFormat($dateTimeFormat, $data);
} else {
$object = \DateTime::class === $type ? \DateTime::createFromFormat($dateTimeFormat, $data, $timezone) : \DateTimeImmutable::createFromFormat($dateTimeFormat, $data, $timezone);
}
$object = \DateTime::class === $type ? \DateTime::createFromFormat($dateTimeFormat, $data, $timezone) : \DateTimeImmutable::createFromFormat($dateTimeFormat, $data, $timezone);
if (false !== $object) {
return $object;
@@ -104,6 +113,16 @@ class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface
throw new NotNormalizableValueException(sprintf('Parsing datetime string "%s" using format "%s" resulted in %d errors: ', $data, $dateTimeFormat, $dateTimeErrors['error_count'])."\n".implode("\n", $this->formatDateTimeErrors($dateTimeErrors['errors'])));
}
$defaultDateTimeFormat = $this->defaultContext[self::FORMAT_KEY] ?? null;
if (null !== $defaultDateTimeFormat) {
$object = \DateTime::class === $type ? \DateTime::createFromFormat($defaultDateTimeFormat, $data, $timezone) : \DateTimeImmutable::createFromFormat($defaultDateTimeFormat, $data, $timezone);
if (false !== $object) {
return $object;
}
}
try {
return \DateTime::class === $type ? new \DateTime($data, $timezone) : new \DateTimeImmutable($data, $timezone);
} catch (\Exception $e) {
@@ -116,7 +135,15 @@ class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface
*/
public function supportsDenormalization($data, $type, $format = null)
{
return isset(self::$supportedTypes[$type]);
return isset(self::SUPPORTED_TYPES[$type]);
}
/**
* {@inheritdoc}
*/
public function hasCacheableSupportsMethod(): bool
{
return __CLASS__ === static::class;
}
/**
@@ -124,7 +151,7 @@ class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface
*
* @return string[]
*/
private function formatDateTimeErrors(array $errors)
private function formatDateTimeErrors(array $errors): array
{
$formattedErrors = [];
@@ -135,9 +162,9 @@ class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface
return $formattedErrors;
}
private function getTimezone(array $context)
private function getTimezone(array $context): ?\DateTimeZone
{
$dateTimeZone = \array_key_exists(self::TIMEZONE_KEY, $context) ? $context[self::TIMEZONE_KEY] : $this->timezone;
$dateTimeZone = $context[self::TIMEZONE_KEY] ?? $this->defaultContext[self::TIMEZONE_KEY];
if (null === $dateTimeZone) {
return null;
@@ -33,8 +33,6 @@ interface DenormalizableInterface
* @param string|null $format The format is optionally given to be able to denormalize
* differently based on different input formats
* @param array $context Options for denormalizing
*
* @return object|object[]
*/
public function denormalize(DenormalizerInterface $denormalizer, $data, $format = null, array $context = []);
}
@@ -21,11 +21,6 @@ trait DenormalizerAwareTrait
*/
protected $denormalizer;
/**
* Sets the Denormalizer.
*
* @param DenormalizerInterface $denormalizer A DenormalizerInterface instance
*/
public function setDenormalizer(DenormalizerInterface $denormalizer)
{
$this->denormalizer = $denormalizer;
@@ -35,14 +35,13 @@ namespace Symfony\Component\Serializer\Normalizer;
class GetSetMethodNormalizer extends AbstractObjectNormalizer
{
private static $setterAccessibleCache = [];
private $cache = [];
/**
* {@inheritdoc}
*/
public function supportsNormalization($data, $format = null)
{
return parent::supportsNormalization($data, $format) && (isset($this->cache[$type = \get_class($data)]) ? $this->cache[$type] : $this->cache[$type] = $this->supports($type));
return parent::supportsNormalization($data, $format) && $this->supports(\get_class($data));
}
/**
@@ -50,17 +49,21 @@ class GetSetMethodNormalizer extends AbstractObjectNormalizer
*/
public function supportsDenormalization($data, $type, $format = null)
{
return parent::supportsDenormalization($data, $type, $format) && (isset($this->cache[$type]) ? $this->cache[$type] : $this->cache[$type] = $this->supports($type));
return parent::supportsDenormalization($data, $type, $format) && $this->supports($type);
}
/**
* Checks if the given class has any get{Property} method.
*
* @param string $class
*
* @return bool
* {@inheritdoc}
*/
private function supports($class)
public function hasCacheableSupportsMethod(): bool
{
return __CLASS__ === static::class;
}
/**
* Checks if the given class has any getter method.
*/
private function supports(string $class): bool
{
$class = new \ReflectionClass($class);
$methods = $class->getMethods(\ReflectionMethod::IS_PUBLIC);
@@ -74,20 +77,18 @@ class GetSetMethodNormalizer extends AbstractObjectNormalizer
}
/**
* Checks if a method's name is get.* or is.*, and can be called without parameters.
*
* @return bool whether the method is a getter or boolean getter
* Checks if a method's name matches /^(get|is|has).+$/ and can be called non-statically without parameters.
*/
private function isGetMethod(\ReflectionMethod $method)
private function isGetMethod(\ReflectionMethod $method): bool
{
$methodLength = \strlen($method->name);
return
!$method->isStatic() &&
(
((0 === strpos($method->name, 'get') && 3 < $methodLength) ||
(0 === strpos($method->name, 'is') && 2 < $methodLength) ||
(0 === strpos($method->name, 'has') && 3 < $methodLength)) &&
((str_starts_with($method->name, 'get') && 3 < $methodLength) ||
(str_starts_with($method->name, 'is') && 2 < $methodLength) ||
(str_starts_with($method->name, 'has') && 3 < $methodLength)) &&
0 === $method->getNumberOfRequiredParameters()
)
;
@@ -107,7 +108,7 @@ class GetSetMethodNormalizer extends AbstractObjectNormalizer
continue;
}
$attributeName = lcfirst(substr($method->name, 0 === strpos($method->name, 'is') ? 2 : 3));
$attributeName = lcfirst(substr($method->name, str_starts_with($method->name, 'is') ? 2 : 3));
if ($this->isAllowedAttribute($object, $attributeName, $format, $context)) {
$attributes[] = $attributeName;
@@ -27,7 +27,7 @@ class JsonSerializableNormalizer extends AbstractNormalizer
public function normalize($object, $format = null, array $context = [])
{
if ($this->isCircularReference($object, $context)) {
return $this->handleCircularReference($object);
return $this->handleCircularReference($object, $format, $context);
}
if (!$object instanceof \JsonSerializable) {
@@ -64,4 +64,12 @@ class JsonSerializableNormalizer extends AbstractNormalizer
{
throw new LogicException(sprintf('Cannot denormalize with "%s".', \JsonSerializable::class));
}
/**
* {@inheritdoc}
*/
public function hasCacheableSupportsMethod(): bool
{
return __CLASS__ === static::class;
}
}
@@ -21,11 +21,6 @@ trait NormalizerAwareTrait
*/
protected $normalizer;
/**
* Sets the normalizer.
*
* @param NormalizerInterface $normalizer A NormalizerInterface instance
*/
public function setNormalizer(NormalizerInterface $normalizer)
{
$this->normalizer = $normalizer;
@@ -28,7 +28,7 @@ interface NormalizerInterface
* @param string $format Format the normalization result will be encoded as
* @param array $context Context options for the normalizer
*
* @return array|string|int|float|bool|null
* @return array|string|int|float|bool|\ArrayObject|null \ArrayObject is used to make sure an empty object is encoded as an object not an array
*
* @throws InvalidArgumentException Occurs when the object given is not a supported type for the normalizer
* @throws CircularReferenceException Occurs when the normalizer detects a circular reference when no circular
+64 -12
View File
@@ -15,7 +15,9 @@ use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\Serializer\Exception\RuntimeException;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
@@ -28,15 +30,31 @@ class ObjectNormalizer extends AbstractObjectNormalizer
{
protected $propertyAccessor;
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null)
private $discriminatorCache = [];
private $objectClassResolver;
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = [])
{
if (!class_exists(PropertyAccess::class)) {
throw new RuntimeException('The ObjectNormalizer class requires the "PropertyAccess" component. Install "symfony/property-access" to use it.');
throw new LogicException('The ObjectNormalizer class requires the "PropertyAccess" component. Install "symfony/property-access" to use it.');
}
parent::__construct($classMetadataFactory, $nameConverter, $propertyTypeExtractor);
parent::__construct($classMetadataFactory, $nameConverter, $propertyTypeExtractor, $classDiscriminatorResolver, $objectClassResolver, $defaultContext);
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
$this->objectClassResolver = $objectClassResolver ?? function ($class) {
return \is_object($class) ? \get_class($class) : $class;
};
}
/**
* {@inheritdoc}
*/
public function hasCacheableSupportsMethod(): bool
{
return __CLASS__ === static::class;
}
/**
@@ -48,7 +66,9 @@ class ObjectNormalizer extends AbstractObjectNormalizer
$attributes = [];
// methods
$reflClass = new \ReflectionClass($object);
$class = ($this->objectClassResolver)($object);
$reflClass = new \ReflectionClass($class);
foreach ($reflClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflMethod) {
if (
0 !== $reflMethod->getNumberOfRequiredParameters() ||
@@ -62,14 +82,14 @@ class ObjectNormalizer extends AbstractObjectNormalizer
$name = $reflMethod->name;
$attributeName = null;
if (0 === strpos($name, 'get') || 0 === strpos($name, 'has')) {
if (str_starts_with($name, 'get') || str_starts_with($name, 'has')) {
// getters and hassers
$attributeName = substr($name, 3);
if (!$reflClass->hasProperty($attributeName)) {
$attributeName = lcfirst($attributeName);
}
} elseif (0 === strpos($name, 'is')) {
} elseif (str_starts_with($name, 'is')) {
// issers
$attributeName = substr($name, 2);
@@ -83,11 +103,9 @@ class ObjectNormalizer extends AbstractObjectNormalizer
}
}
$checkPropertyInitialization = \PHP_VERSION_ID >= 70400;
// properties
foreach ($reflClass->getProperties(\ReflectionProperty::IS_PUBLIC) as $reflProperty) {
if ($checkPropertyInitialization && !$reflProperty->isInitialized($object)) {
foreach ($reflClass->getProperties() as $reflProperty) {
if (!$reflProperty->isPublic()) {
continue;
}
@@ -106,7 +124,16 @@ class ObjectNormalizer extends AbstractObjectNormalizer
*/
protected function getAttributeValue($object, $attribute, $format = null, array $context = [])
{
return $this->propertyAccessor->getValue($object, $attribute);
$cacheKey = \get_class($object);
if (!\array_key_exists($cacheKey, $this->discriminatorCache)) {
$this->discriminatorCache[$cacheKey] = null;
if (null !== $this->classDiscriminatorResolver) {
$mapping = $this->classDiscriminatorResolver->getMappingForMappedObject($object);
$this->discriminatorCache[$cacheKey] = null === $mapping ? null : $mapping->getTypeProperty();
}
}
return $attribute === $this->discriminatorCache[$cacheKey] ? $this->classDiscriminatorResolver->getTypeForMappedObject($object) : $this->propertyAccessor->getValue($object, $attribute);
}
/**
@@ -120,4 +147,29 @@ class ObjectNormalizer extends AbstractObjectNormalizer
// Properties not found are ignored
}
}
/**
* {@inheritdoc}
*/
protected function getAllowedAttributes($classOrObject, array $context, $attributesAsString = false)
{
if (false === $allowedAttributes = parent::getAllowedAttributes($classOrObject, $context, $attributesAsString)) {
return false;
}
if (null !== $this->classDiscriminatorResolver) {
$class = \is_object($classOrObject) ? \get_class($classOrObject) : $classOrObject;
if (null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForMappedObject($classOrObject)) {
$allowedAttributes[] = $attributesAsString ? $discriminatorMapping->getTypeProperty() : new AttributeMetadata($discriminatorMapping->getTypeProperty());
}
if (null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForClass($class)) {
foreach ($discriminatorMapping->getTypesMapping() as $mappedClass) {
$allowedAttributes = array_merge($allowedAttributes, parent::getAllowedAttributes($mappedClass, $context, $attributesAsString));
}
}
}
return $allowedAttributes;
}
}
@@ -25,7 +25,7 @@ trait ObjectToPopulateTrait
*/
protected function extractObjectToPopulate($class, array $context, $key = null)
{
$key = $key ?: 'object_to_populate';
$key = $key ?? AbstractNormalizer::OBJECT_TO_POPULATE;
if (isset($context[$key]) && \is_object($context[$key]) && $context[$key] instanceof $class) {
return $context[$key];
+30 -25
View File
@@ -11,6 +11,8 @@
namespace Symfony\Component\Serializer\Normalizer;
use Symfony\Component\PropertyAccess\Exception\AccessException;
/**
* Converts between objects and arrays by mapping properties.
*
@@ -30,14 +32,12 @@ namespace Symfony\Component\Serializer\Normalizer;
*/
class PropertyNormalizer extends AbstractObjectNormalizer
{
private $cache = [];
/**
* {@inheritdoc}
*/
public function supportsNormalization($data, $format = null)
{
return parent::supportsNormalization($data, $format) && (isset($this->cache[$type = \get_class($data)]) ? $this->cache[$type] : $this->cache[$type] = $this->supports($type));
return parent::supportsNormalization($data, $format) && $this->supports(\get_class($data));
}
/**
@@ -45,17 +45,21 @@ class PropertyNormalizer extends AbstractObjectNormalizer
*/
public function supportsDenormalization($data, $type, $format = null)
{
return parent::supportsDenormalization($data, $type, $format) && (isset($this->cache[$type]) ? $this->cache[$type] : $this->cache[$type] = $this->supports($type));
return parent::supportsDenormalization($data, $type, $format) && $this->supports($type);
}
/**
* {@inheritdoc}
*/
public function hasCacheableSupportsMethod(): bool
{
return __CLASS__ === static::class;
}
/**
* Checks if the given class has any non-static property.
*
* @param string $class
*
* @return bool
*/
private function supports($class)
private function supports(string $class): bool
{
$class = new \ReflectionClass($class);
@@ -99,20 +103,9 @@ class PropertyNormalizer extends AbstractObjectNormalizer
{
$reflectionObject = new \ReflectionObject($object);
$attributes = [];
$checkPropertyInitialization = \PHP_VERSION_ID >= 70400;
do {
foreach ($reflectionObject->getProperties() as $property) {
if ($checkPropertyInitialization) {
if (!$property->isPublic()) {
$property->setAccessible(true);
}
if (!$property->isInitialized($object)) {
continue;
}
}
if (!$this->isAllowedAttribute($reflectionObject->getName(), $property->name, $format, $context)) {
continue;
}
@@ -121,7 +114,7 @@ class PropertyNormalizer extends AbstractObjectNormalizer
}
} while ($reflectionObject = $reflectionObject->getParentClass());
return $attributes;
return array_unique($attributes);
}
/**
@@ -140,6 +133,21 @@ class PropertyNormalizer extends AbstractObjectNormalizer
$reflectionProperty->setAccessible(true);
}
if (\PHP_VERSION_ID >= 70400 && $reflectionProperty->hasType()) {
return $reflectionProperty->getValue($object);
}
if (!method_exists($object, '__get') && !isset($object->$attribute)) {
$propertyValues = (array) $object;
if (($reflectionProperty->isPublic() && !\array_key_exists($reflectionProperty->name, $propertyValues))
|| ($reflectionProperty->isProtected() && !\array_key_exists("\0*\0{$reflectionProperty->name}", $propertyValues))
|| ($reflectionProperty->isPrivate() && !\array_key_exists("\0{$reflectionProperty->class}\0{$reflectionProperty->name}", $propertyValues))
) {
throw new AccessException(sprintf('The property "%s::$%s" is not initialized.', \get_class($object), $reflectionProperty->name));
}
}
return $reflectionProperty->getValue($object);
}
@@ -168,13 +176,10 @@ class PropertyNormalizer extends AbstractObjectNormalizer
/**
* @param string|object $classOrObject
* @param string $attribute
*
* @return \ReflectionProperty
*
* @throws \ReflectionException
*/
private function getReflectionProperty($classOrObject, $attribute)
private function getReflectionProperty($classOrObject, string $attribute): \ReflectionProperty
{
$reflectionClass = new \ReflectionClass($classOrObject);
while (true) {
@@ -1,27 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Normalizer;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerAwareTrait;
/**
* SerializerAware Normalizer implementation.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @deprecated since version 3.1, to be removed in 4.0. Use the SerializerAwareTrait instead.
*/
abstract class SerializerAwareNormalizer implements SerializerAwareInterface
{
use SerializerAwareTrait;
}
+8 -9
View File
@@ -1,16 +1,15 @@
Serializer Component
====================
With the Serializer component it's possible to handle serializing data
structures, including object graphs, into array structures or other formats like
XML and JSON. It can also handle deserializing XML and JSON back to object
graphs.
The Serializer component handles serializing and deserializing data structures,
including object graphs, into array structures or other formats like XML and
JSON.
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/serializer.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)
* [Documentation](https://symfony.com/doc/current/components/serializer.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)
+86 -82
View File
@@ -13,11 +13,17 @@ namespace Symfony\Component\Serializer;
use Symfony\Component\Serializer\Encoder\ChainDecoder;
use Symfony\Component\Serializer\Encoder\ChainEncoder;
use Symfony\Component\Serializer\Encoder\ContextAwareDecoderInterface;
use Symfony\Component\Serializer\Encoder\ContextAwareEncoderInterface;
use Symfony\Component\Serializer\Encoder\DecoderInterface;
use Symfony\Component\Serializer\Encoder\EncoderInterface;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Exception\NotEncodableValueException;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
@@ -38,7 +44,7 @@ use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
* @author Lukas Kahwe Smith <smith@pooteeweet.org>
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class Serializer implements SerializerInterface, NormalizerInterface, DenormalizerInterface, EncoderInterface, DecoderInterface
class Serializer implements SerializerInterface, ContextAwareNormalizerInterface, ContextAwareDenormalizerInterface, ContextAwareEncoderInterface, ContextAwareDecoderInterface
{
/**
* @var Encoder\ChainEncoder
@@ -51,24 +57,18 @@ class Serializer implements SerializerInterface, NormalizerInterface, Denormaliz
protected $decoder;
/**
* @var array
* @internal since Symfony 4.1
*/
protected $normalizers = [];
/**
* @var array
*
* @deprecated since 3.1 will be removed in 4.0
*/
protected $normalizerCache = [];
private $cachedNormalizers;
private $denormalizerCache = [];
private $normalizerCache = [];
/**
* @var array
*
* @deprecated since 3.1 will be removed in 4.0
* @param array<NormalizerInterface|DenormalizerInterface> $normalizers
* @param array<EncoderInterface|DecoderInterface> $encoders
*/
protected $denormalizerCache = [];
public function __construct(array $normalizers = [], array $encoders = [])
{
foreach ($normalizers as $normalizer) {
@@ -83,6 +83,11 @@ class Serializer implements SerializerInterface, NormalizerInterface, Denormaliz
if ($normalizer instanceof NormalizerAwareInterface) {
$normalizer->setNormalizer($this);
}
if (!($normalizer instanceof NormalizerInterface || $normalizer instanceof DenormalizerInterface)) {
@trigger_error(sprintf('Passing normalizers ("%s") which do not implement either "%s" or "%s" has been deprecated since Symfony 4.2.', \get_class($normalizer), NormalizerInterface::class, DenormalizerInterface::class), \E_USER_DEPRECATED);
// throw new \InvalidArgumentException(sprintf('The class "%s" does not implement "%s" or "%s".', \get_class($normalizer), NormalizerInterface::class, DenormalizerInterface::class));
}
}
$this->normalizers = $normalizers;
@@ -98,6 +103,11 @@ class Serializer implements SerializerInterface, NormalizerInterface, Denormaliz
if ($encoder instanceof EncoderInterface) {
$realEncoders[] = $encoder;
}
if (!($encoder instanceof EncoderInterface || $encoder instanceof DecoderInterface)) {
@trigger_error(sprintf('Passing encoders ("%s") which do not implement either "%s" or "%s" has been deprecated since Symfony 4.2.', \get_class($encoder), EncoderInterface::class, DecoderInterface::class), \E_USER_DEPRECATED);
// throw new \InvalidArgumentException(sprintf('The class "%s" does not implement "%s" or "%s".', \get_class($normalizer), EncoderInterface::class, DecoderInterface::class));
}
}
$this->encoder = new ChainEncoder($realEncoders);
$this->decoder = new ChainDecoder($decoders);
@@ -106,7 +116,7 @@ class Serializer implements SerializerInterface, NormalizerInterface, Denormaliz
/**
* {@inheritdoc}
*/
final public function serialize($data, $format, array $context = [])
final public function serialize($data, $format, array $context = []): string
{
if (!$this->supportsEncoding($format, $context)) {
throw new NotEncodableValueException(sprintf('Serialization for the format "%s" is not supported.', $format));
@@ -143,11 +153,15 @@ class Serializer implements SerializerInterface, NormalizerInterface, Denormaliz
return $normalizer->normalize($data, $format, $context);
}
if (null === $data || is_scalar($data)) {
if (null === $data || \is_scalar($data)) {
return $data;
}
if (\is_array($data) || $data instanceof \Traversable) {
if (is_iterable($data)) {
if (($context[AbstractObjectNormalizer::PRESERVE_EMPTY_OBJECTS] ?? false) === true && $data instanceof \Countable && 0 === $data->count()) {
return $data;
}
$normalized = [];
foreach ($data as $key => $val) {
$normalized[$key] = $this->normalize($val, $format, $context);
@@ -188,42 +202,16 @@ class Serializer implements SerializerInterface, NormalizerInterface, Denormaliz
/**
* {@inheritdoc}
*/
public function supportsNormalization($data, $format = null/*, array $context = []*/)
public function supportsNormalization($data, $format = null, array $context = [])
{
if (\func_num_args() > 2) {
$context = func_get_arg(2);
} else {
if (__CLASS__ !== static::class) {
$r = new \ReflectionMethod($this, __FUNCTION__);
if (__CLASS__ !== $r->getDeclaringClass()->getName()) {
@trigger_error(sprintf('The "%s()" method will have a third `$context = []` argument in version 4.0. Not defining it is deprecated since Symfony 3.3.', __METHOD__), \E_USER_DEPRECATED);
}
}
$context = [];
}
return null !== $this->getNormalizer($data, $format, $context);
}
/**
* {@inheritdoc}
*/
public function supportsDenormalization($data, $type, $format = null/*, array $context = []*/)
public function supportsDenormalization($data, $type, $format = null, array $context = [])
{
if (\func_num_args() > 3) {
$context = func_get_arg(3);
} else {
if (__CLASS__ !== static::class) {
$r = new \ReflectionMethod($this, __FUNCTION__);
if (__CLASS__ !== $r->getDeclaringClass()->getName()) {
@trigger_error(sprintf('The "%s()" method will have a fourth `$context = []` argument in version 4.0. Not defining it is deprecated since Symfony 3.3.', __METHOD__), \E_USER_DEPRECATED);
}
}
$context = [];
}
return null !== $this->getDenormalizer($data, $type, $format, $context);
}
@@ -233,13 +221,35 @@ class Serializer implements SerializerInterface, NormalizerInterface, Denormaliz
* @param mixed $data Data to get the serializer for
* @param string $format Format name, present to give the option to normalizers to act differently based on formats
* @param array $context Options available to the normalizer
*
* @return NormalizerInterface|null
*/
private function getNormalizer($data, $format, array $context)
private function getNormalizer($data, ?string $format, array $context): ?NormalizerInterface
{
foreach ($this->normalizers as $normalizer) {
if ($normalizer instanceof NormalizerInterface && $normalizer->supportsNormalization($data, $format, $context)) {
if ($this->cachedNormalizers !== $this->normalizers) {
$this->cachedNormalizers = $this->normalizers;
$this->denormalizerCache = $this->normalizerCache = [];
}
$type = \is_object($data) ? \get_class($data) : 'native-'.\gettype($data);
if (!isset($this->normalizerCache[$format][$type])) {
$this->normalizerCache[$format][$type] = [];
foreach ($this->normalizers as $k => $normalizer) {
if (!$normalizer instanceof NormalizerInterface) {
continue;
}
if (!$normalizer instanceof CacheableSupportsMethodInterface || !$normalizer->hasCacheableSupportsMethod()) {
$this->normalizerCache[$format][$type][$k] = false;
} elseif ($normalizer->supportsNormalization($data, $format, $context)) {
$this->normalizerCache[$format][$type][$k] = true;
break;
}
}
}
foreach ($this->normalizerCache[$format][$type] as $k => $cached) {
$normalizer = $this->normalizers[$k];
if ($cached || $normalizer->supportsNormalization($data, $format, $context)) {
return $normalizer;
}
}
@@ -254,13 +264,33 @@ class Serializer implements SerializerInterface, NormalizerInterface, Denormaliz
* @param string $class The expected class to instantiate
* @param string $format Format name, present to give the option to normalizers to act differently based on formats
* @param array $context Options available to the denormalizer
*
* @return DenormalizerInterface|null
*/
private function getDenormalizer($data, $class, $format, array $context)
private function getDenormalizer($data, string $class, ?string $format, array $context): ?DenormalizerInterface
{
foreach ($this->normalizers as $normalizer) {
if ($normalizer instanceof DenormalizerInterface && $normalizer->supportsDenormalization($data, $class, $format, $context)) {
if ($this->cachedNormalizers !== $this->normalizers) {
$this->cachedNormalizers = $this->normalizers;
$this->denormalizerCache = $this->normalizerCache = [];
}
if (!isset($this->denormalizerCache[$format][$class])) {
$this->denormalizerCache[$format][$class] = [];
foreach ($this->normalizers as $k => $normalizer) {
if (!$normalizer instanceof DenormalizerInterface) {
continue;
}
if (!$normalizer instanceof CacheableSupportsMethodInterface || !$normalizer->hasCacheableSupportsMethod()) {
$this->denormalizerCache[$format][$class][$k] = false;
} elseif ($normalizer->supportsDenormalization(null, $class, $format, $context)) {
$this->denormalizerCache[$format][$class][$k] = true;
break;
}
}
}
foreach ($this->denormalizerCache[$format][$class] as $k => $cached) {
$normalizer = $this->normalizers[$k];
if ($cached || $normalizer->supportsDenormalization($data, $class, $format, $context)) {
return $normalizer;
}
}
@@ -287,42 +317,16 @@ class Serializer implements SerializerInterface, NormalizerInterface, Denormaliz
/**
* {@inheritdoc}
*/
public function supportsEncoding($format/*, array $context = []*/)
public function supportsEncoding($format, array $context = [])
{
if (\func_num_args() > 1) {
$context = func_get_arg(1);
} else {
if (__CLASS__ !== static::class) {
$r = new \ReflectionMethod($this, __FUNCTION__);
if (__CLASS__ !== $r->getDeclaringClass()->getName()) {
@trigger_error(sprintf('The "%s()" method will have a second `$context = []` argument in version 4.0. Not defining it is deprecated since Symfony 3.3.', __METHOD__), \E_USER_DEPRECATED);
}
}
$context = [];
}
return $this->encoder->supportsEncoding($format, $context);
}
/**
* {@inheritdoc}
*/
public function supportsDecoding($format/*, array $context = []*/)
public function supportsDecoding($format, array $context = [])
{
if (\func_num_args() > 1) {
$context = func_get_arg(1);
} else {
if (__CLASS__ !== static::class) {
$r = new \ReflectionMethod($this, __FUNCTION__);
if (__CLASS__ !== $r->getDeclaringClass()->getName()) {
@trigger_error(sprintf('The "%s()" method will have a second `$context = []` argument in version 4.0. Not defining it is deprecated since Symfony 3.3.', __METHOD__), \E_USER_DEPRECATED);
}
}
$context = [];
}
return $this->decoder->supportsDecoding($format, $context);
}
}
-5
View File
@@ -21,11 +21,6 @@ trait SerializerAwareTrait
*/
protected $serializer;
/**
* Sets the serializer.
*
* @param SerializerInterface $serializer A SerializerInterface instance
*/
public function setSerializer(SerializerInterface $serializer)
{
$this->serializer = $serializer;
@@ -1,53 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Annotation;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class GroupsTest extends TestCase
{
public function testEmptyGroupsParameter()
{
$this->expectException('Symfony\Component\Serializer\Exception\InvalidArgumentException');
new Groups(['value' => []]);
}
public function testNotAnArrayGroupsParameter()
{
$this->expectException('Symfony\Component\Serializer\Exception\InvalidArgumentException');
new Groups(['value' => 12]);
}
public function testInvalidGroupsParameter()
{
$this->expectException('Symfony\Component\Serializer\Exception\InvalidArgumentException');
new Groups(['value' => ['a', 1, new \stdClass()]]);
}
public function testGroupsParameters()
{
$validData = ['a', 'b'];
$groups = new Groups(['value' => $validData]);
$this->assertEquals($validData, $groups->getGroups());
}
public function testSingleGroup()
{
$groups = new Groups(['value' => 'a']);
$this->assertEquals(['a'], $groups->getGroups());
}
}
@@ -1,54 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Annotation;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Annotation\MaxDepth;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class MaxDepthTest extends TestCase
{
public function testNotSetMaxDepthParameter()
{
$this->expectException('Symfony\Component\Serializer\Exception\InvalidArgumentException');
$this->expectExceptionMessage('Parameter of annotation "Symfony\Component\Serializer\Annotation\MaxDepth" should be set.');
new MaxDepth([]);
}
public function provideInvalidValues()
{
return [
[''],
['foo'],
['1'],
[0],
];
}
/**
* @dataProvider provideInvalidValues
*/
public function testNotAnIntMaxDepthParameter($value)
{
$this->expectException('Symfony\Component\Serializer\Exception\InvalidArgumentException');
$this->expectExceptionMessage('Parameter of annotation "Symfony\Component\Serializer\Annotation\MaxDepth" must be a positive integer.');
new MaxDepth(['value' => $value]);
}
public function testMaxDepthParameters()
{
$maxDepth = new MaxDepth(['value' => 3]);
$this->assertEquals(3, $maxDepth->getMaxDepth());
}
}
@@ -1,71 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\DependencyInjection;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Serializer\DependencyInjection\SerializerPass;
/**
* Tests for the SerializerPass class.
*
* @author Javier Lopez <f12loalf@gmail.com>
*/
class SerializerPassTest extends TestCase
{
public function testThrowExceptionWhenNoNormalizers()
{
$this->expectException('RuntimeException');
$this->expectExceptionMessage('You must tag at least one service as "serializer.normalizer" to use the "serializer" service');
$container = new ContainerBuilder();
$container->register('serializer');
$serializerPass = new SerializerPass();
$serializerPass->process($container);
}
public function testThrowExceptionWhenNoEncoders()
{
$this->expectException('RuntimeException');
$this->expectExceptionMessage('You must tag at least one service as "serializer.encoder" to use the "serializer" service');
$container = new ContainerBuilder();
$container->register('serializer')
->addArgument([])
->addArgument([]);
$container->register('normalizer')->addTag('serializer.normalizer');
$serializerPass = new SerializerPass();
$serializerPass->process($container);
}
public function testServicesAreOrderedAccordingToPriority()
{
$container = new ContainerBuilder();
$definition = $container->register('serializer')->setArguments([null, null]);
$container->register('n2')->addTag('serializer.normalizer', ['priority' => 100])->addTag('serializer.encoder', ['priority' => 100]);
$container->register('n1')->addTag('serializer.normalizer', ['priority' => 200])->addTag('serializer.encoder', ['priority' => 200]);
$container->register('n3')->addTag('serializer.normalizer')->addTag('serializer.encoder');
$serializerPass = new SerializerPass();
$serializerPass->process($container);
$expected = [
new Reference('n1'),
new Reference('n2'),
new Reference('n3'),
];
$this->assertEquals($expected, $definition->getArgument(0));
$this->assertEquals($expected, $definition->getArgument(1));
}
}
@@ -1,128 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
class DeserializeNestedArrayOfObjectsTest extends TestCase
{
public function provider()
{
return [
//from property PhpDoc
[Zoo::class],
//from argument constructor PhpDoc
[ZooImmutable::class],
];
}
/**
* @dataProvider provider
*/
public function testPropertyPhpDoc($class)
{
//GIVEN
$json = <<<EOF
{
"animals": [
{"name": "Bug"}
]
}
EOF;
$serializer = new Serializer([
new ObjectNormalizer(null, null, null, new PhpDocExtractor()),
new ArrayDenormalizer(),
], ['json' => new JsonEncoder()]);
//WHEN
/** @var Zoo $zoo */
$zoo = $serializer->deserialize($json, $class, 'json');
//THEN
self::assertCount(1, $zoo->getAnimals());
self::assertInstanceOf(Animal::class, $zoo->getAnimals()[0]);
}
}
class Zoo
{
/** @var Animal[] */
private $animals = [];
/**
* @return Animal[]
*/
public function getAnimals()
{
return $this->animals;
}
/**
* @param Animal[] $animals
*/
public function setAnimals(array $animals)
{
$this->animals = $animals;
}
}
class ZooImmutable
{
/** @var Animal[] */
private $animals = [];
/**
* @param Animal[] $animals
*/
public function __construct(array $animals = [])
{
$this->animals = $animals;
}
/**
* @return Animal[]
*/
public function getAnimals()
{
return $this->animals;
}
}
class Animal
{
/** @var string */
private $name;
public function __construct()
{
echo '';
}
/**
* @return string|null
*/
public function getName()
{
return $this->name;
}
/**
* @param string|null $name
*/
public function setName($name)
{
$this->name = $name;
}
}
@@ -1,78 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Encoder;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Encoder\ChainDecoder;
class ChainDecoderTest extends TestCase
{
const FORMAT_1 = 'format1';
const FORMAT_2 = 'format2';
const FORMAT_3 = 'format3';
private $chainDecoder;
private $decoder1;
private $decoder2;
protected function setUp()
{
$this->decoder1 = $this
->getMockBuilder('Symfony\Component\Serializer\Encoder\DecoderInterface')
->getMock();
$this->decoder1
->method('supportsDecoding')
->willReturnMap([
[self::FORMAT_1, [], true],
[self::FORMAT_2, [], false],
[self::FORMAT_3, [], false],
[self::FORMAT_3, ['foo' => 'bar'], true],
]);
$this->decoder2 = $this
->getMockBuilder('Symfony\Component\Serializer\Encoder\DecoderInterface')
->getMock();
$this->decoder2
->method('supportsDecoding')
->willReturnMap([
[self::FORMAT_1, [], false],
[self::FORMAT_2, [], true],
[self::FORMAT_3, [], false],
]);
$this->chainDecoder = new ChainDecoder([$this->decoder1, $this->decoder2]);
}
public function testSupportsDecoding()
{
$this->assertTrue($this->chainDecoder->supportsDecoding(self::FORMAT_1));
$this->assertTrue($this->chainDecoder->supportsDecoding(self::FORMAT_2));
$this->assertFalse($this->chainDecoder->supportsDecoding(self::FORMAT_3));
$this->assertTrue($this->chainDecoder->supportsDecoding(self::FORMAT_3, ['foo' => 'bar']));
}
public function testDecode()
{
$this->decoder1->expects($this->never())->method('decode');
$this->decoder2->expects($this->once())->method('decode');
$this->chainDecoder->decode('string_to_decode', self::FORMAT_2);
}
public function testDecodeUnsupportedFormat()
{
$this->expectException('Symfony\Component\Serializer\Exception\RuntimeException');
$this->chainDecoder->decode('string_to_decode', self::FORMAT_3);
}
}
@@ -1,106 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Encoder;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Encoder\ChainEncoder;
use Symfony\Component\Serializer\Encoder\EncoderInterface;
use Symfony\Component\Serializer\Encoder\NormalizationAwareInterface;
class ChainEncoderTest extends TestCase
{
const FORMAT_1 = 'format1';
const FORMAT_2 = 'format2';
const FORMAT_3 = 'format3';
private $chainEncoder;
private $encoder1;
private $encoder2;
protected function setUp()
{
$this->encoder1 = $this
->getMockBuilder('Symfony\Component\Serializer\Encoder\EncoderInterface')
->getMock();
$this->encoder1
->method('supportsEncoding')
->willReturnMap([
[self::FORMAT_1, [], true],
[self::FORMAT_2, [], false],
[self::FORMAT_3, [], false],
[self::FORMAT_3, ['foo' => 'bar'], true],
]);
$this->encoder2 = $this
->getMockBuilder('Symfony\Component\Serializer\Encoder\EncoderInterface')
->getMock();
$this->encoder2
->method('supportsEncoding')
->willReturnMap([
[self::FORMAT_1, [], false],
[self::FORMAT_2, [], true],
[self::FORMAT_3, [], false],
]);
$this->chainEncoder = new ChainEncoder([$this->encoder1, $this->encoder2]);
}
public function testSupportsEncoding()
{
$this->assertTrue($this->chainEncoder->supportsEncoding(self::FORMAT_1));
$this->assertTrue($this->chainEncoder->supportsEncoding(self::FORMAT_2));
$this->assertFalse($this->chainEncoder->supportsEncoding(self::FORMAT_3));
$this->assertTrue($this->chainEncoder->supportsEncoding(self::FORMAT_3, ['foo' => 'bar']));
}
public function testEncode()
{
$this->encoder1->expects($this->never())->method('encode');
$this->encoder2->expects($this->once())->method('encode');
$this->chainEncoder->encode(['foo' => 123], self::FORMAT_2);
}
public function testEncodeUnsupportedFormat()
{
$this->expectException('Symfony\Component\Serializer\Exception\RuntimeException');
$this->chainEncoder->encode(['foo' => 123], self::FORMAT_3);
}
public function testNeedsNormalizationBasic()
{
$this->assertTrue($this->chainEncoder->needsNormalization(self::FORMAT_1));
$this->assertTrue($this->chainEncoder->needsNormalization(self::FORMAT_2));
}
public function testNeedsNormalizationNormalizationAware()
{
$encoder = new NormalizationAwareEncoder();
$sut = new ChainEncoder([$encoder]);
$this->assertFalse($sut->needsNormalization(self::FORMAT_1));
}
}
class NormalizationAwareEncoder implements EncoderInterface, NormalizationAwareInterface
{
public function supportsEncoding($format)
{
return true;
}
public function encode($data, $format, array $context = [])
{
}
}
@@ -1,348 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Encoder;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Encoder\CsvEncoder;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class CsvEncoderTest extends TestCase
{
/**
* @var CsvEncoder
*/
private $encoder;
protected function setUp()
{
$this->encoder = new CsvEncoder();
}
public function testTrueFalseValues()
{
$data = [
'string' => 'foo',
'int' => 2,
'false' => false,
'true' => true,
'int_one' => 1,
'string_one' => '1',
];
// Check that true and false are appropriately handled
$this->assertSame($csv = <<<'CSV'
string,int,false,true,int_one,string_one
foo,2,0,1,1,1
CSV
, $this->encoder->encode($data, 'csv'));
$this->assertSame([
'string' => 'foo',
'int' => '2',
'false' => '0',
'true' => '1',
'int_one' => '1',
'string_one' => '1',
], $this->encoder->decode($csv, 'csv'));
}
/**
* @requires PHP 7.4
*/
public function testDoubleQuotesAndSlashes()
{
$this->assertSame($csv = <<<'CSV'
0,1,2,3,4,5
,"""","foo""","\""",\,foo\
CSV
, $this->encoder->encode($data = ['', '"', 'foo"', '\\"', '\\', 'foo\\'], 'csv'));
$this->assertSame($data, $this->encoder->decode($csv, 'csv'));
}
/**
* @requires PHP 7.4
*/
public function testSingleSlash()
{
$this->assertSame($csv = "0\n\\\n", $this->encoder->encode($data = ['\\'], 'csv'));
$this->assertSame($data, $this->encoder->decode($csv, 'csv'));
$this->assertSame($data, $this->encoder->decode(trim($csv), 'csv'));
}
public function testSupportEncoding()
{
$this->assertTrue($this->encoder->supportsEncoding('csv'));
$this->assertFalse($this->encoder->supportsEncoding('foo'));
}
public function testEncode()
{
$value = ['foo' => 'hello', 'bar' => 'hey ho'];
$this->assertEquals(<<<'CSV'
foo,bar
hello,"hey ho"
CSV
, $this->encoder->encode($value, 'csv'));
}
public function testEncodeCollection()
{
$value = [
['foo' => 'hello', 'bar' => 'hey ho'],
['foo' => 'hi', 'bar' => 'let\'s go'],
];
$this->assertEquals(<<<'CSV'
foo,bar
hello,"hey ho"
hi,"let's go"
CSV
, $this->encoder->encode($value, 'csv'));
}
public function testEncodePlainIndexedArray()
{
$this->assertEquals(<<<'CSV'
0,1,2
a,b,c
CSV
, $this->encoder->encode(['a', 'b', 'c'], 'csv'));
}
public function testEncodeNonArray()
{
$this->assertEquals(<<<'CSV'
0
foo
CSV
, $this->encoder->encode('foo', 'csv'));
}
public function testEncodeNestedArrays()
{
$value = ['foo' => 'hello', 'bar' => [
['id' => 'yo', 1 => 'wesh'],
['baz' => 'Halo', 'foo' => 'olá'],
]];
$this->assertEquals(<<<'CSV'
foo,bar.0.id,bar.0.1,bar.1.baz,bar.1.foo
hello,yo,wesh,Halo,olá
CSV
, $this->encoder->encode($value, 'csv'));
}
public function testEncodeCustomSettings()
{
$this->encoder = new CsvEncoder(';', "'", '|', '-');
$value = ['a' => 'he\'llo', 'c' => ['d' => 'foo']];
$this->assertEquals(<<<'CSV'
a;c-d
'he''llo';foo
CSV
, $this->encoder->encode($value, 'csv'));
}
public function testEncodeCustomSettingsPassedInContext()
{
$value = ['a' => 'he\'llo', 'c' => ['d' => 'foo']];
$this->assertSame(<<<'CSV'
a;c-d
'he''llo';foo
CSV
, $this->encoder->encode($value, 'csv', [
CsvEncoder::DELIMITER_KEY => ';',
CsvEncoder::ENCLOSURE_KEY => "'",
CsvEncoder::ESCAPE_CHAR_KEY => '|',
CsvEncoder::KEY_SEPARATOR_KEY => '-',
]));
}
public function testEncodeEmptyArray()
{
$this->assertEquals("\n\n", $this->encoder->encode([], 'csv'));
$this->assertEquals("\n\n", $this->encoder->encode([[]], 'csv'));
}
public function testEncodeVariableStructure()
{
$value = [
['a' => ['foo', 'bar']],
['a' => [], 'b' => 'baz'],
['a' => ['bar', 'foo'], 'c' => 'pong'],
];
$csv = <<<CSV
a.0,a.1,c,b
foo,bar,,
,,,baz
bar,foo,pong,
CSV;
$this->assertEquals($csv, $this->encoder->encode($value, 'csv'));
}
public function testEncodeCustomHeaders()
{
$context = [
CsvEncoder::HEADERS_KEY => [
'b',
'c',
],
];
$value = [
['a' => 'foo', 'b' => 'bar'],
];
$csv = <<<CSV
b,c,a
bar,,foo
CSV;
$this->assertEquals($csv, $this->encoder->encode($value, 'csv', $context));
}
public function testSupportsDecoding()
{
$this->assertTrue($this->encoder->supportsDecoding('csv'));
$this->assertFalse($this->encoder->supportsDecoding('foo'));
}
public function testDecode()
{
$expected = ['foo' => 'a', 'bar' => 'b'];
$this->assertEquals($expected, $this->encoder->decode(<<<'CSV'
foo,bar
a,b
CSV
, 'csv'));
}
public function testDecodeCollection()
{
$expected = [
['foo' => 'a', 'bar' => 'b'],
['foo' => 'c', 'bar' => 'd'],
['foo' => 'f'],
];
$this->assertEquals($expected, $this->encoder->decode(<<<'CSV'
foo,bar
a,b
c,d
f
CSV
, 'csv'));
}
public function testDecodeToManyRelation()
{
$expected = [
['foo' => 'bar', 'relations' => [['a' => 'b'], ['a' => 'b']]],
['foo' => 'bat', 'relations' => [['a' => 'b'], ['a' => '']]],
['foo' => 'bat', 'relations' => [['a' => 'b']]],
['foo' => 'baz', 'relations' => [['a' => 'c'], ['a' => 'c']]],
];
$this->assertEquals($expected, $this->encoder->decode(<<<'CSV'
foo,relations.0.a,relations.1.a
bar,b,b
bat,b,
bat,b
baz,c,c
CSV
, 'csv'));
}
public function testDecodeNestedArrays()
{
$expected = [
['foo' => 'a', 'bar' => ['baz' => ['bat' => 'b']]],
['foo' => 'c', 'bar' => ['baz' => ['bat' => 'd']]],
];
$this->assertEquals($expected, $this->encoder->decode(<<<'CSV'
foo,bar.baz.bat
a,b
c,d
CSV
, 'csv'));
}
public function testDecodeCustomSettings()
{
$this->encoder = new CsvEncoder(';', "'", '|', '-');
$expected = ['a' => 'hell\'o', 'bar' => ['baz' => 'b']];
$this->assertEquals($expected, $this->encoder->decode(<<<'CSV'
a;bar-baz
'hell''o';b;c
CSV
, 'csv'));
}
public function testDecodeCustomSettingsPassedInContext()
{
$expected = ['a' => 'hell\'o', 'bar' => ['baz' => 'b']];
$this->assertEquals($expected, $this->encoder->decode(<<<'CSV'
a;bar-baz
'hell''o';b;c
CSV
, 'csv', [
CsvEncoder::DELIMITER_KEY => ';',
CsvEncoder::ENCLOSURE_KEY => "'",
CsvEncoder::ESCAPE_CHAR_KEY => '|',
CsvEncoder::KEY_SEPARATOR_KEY => '-',
]));
}
public function testDecodeMalformedCollection()
{
$expected = [
['foo' => 'a', 'bar' => 'b'],
['foo' => 'c', 'bar' => 'd'],
['foo' => 'f'],
];
$this->assertEquals($expected, $this->encoder->decode(<<<'CSV'
foo,bar
a,b,e
c,d,g,h
f
CSV
, 'csv'));
}
public function testDecodeEmptyArray()
{
$this->assertEquals([], $this->encoder->decode('', 'csv'));
}
}
@@ -1,75 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Encoder;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Encoder\JsonDecode;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
class JsonDecodeTest extends TestCase
{
/** @var \Symfony\Component\Serializer\Encoder\JsonDecode */
private $decode;
protected function setUp()
{
$this->decode = new JsonDecode();
}
public function testSupportsDecoding()
{
$this->assertTrue($this->decode->supportsDecoding(JsonEncoder::FORMAT));
$this->assertFalse($this->decode->supportsDecoding('foobar'));
}
/**
* @dataProvider decodeProvider
*/
public function testDecode($toDecode, $expected, $context)
{
$this->assertEquals(
$expected,
$this->decode->decode($toDecode, JsonEncoder::FORMAT, $context)
);
}
public function decodeProvider()
{
$stdClass = new \stdClass();
$stdClass->foo = 'bar';
$assoc = ['foo' => 'bar'];
return [
['{"foo": "bar"}', $stdClass, []],
['{"foo": "bar"}', $assoc, ['json_decode_associative' => true]],
];
}
/**
* @requires function json_last_error_msg
* @dataProvider decodeProviderException
*/
public function testDecodeWithException($value)
{
$this->expectException('Symfony\Component\Serializer\Exception\UnexpectedValueException');
$this->decode->decode($value, JsonEncoder::FORMAT);
}
public function decodeProviderException()
{
return [
["{'foo': 'bar'}"],
['kaboom!'],
];
}
}
@@ -1,60 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Encoder;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
class JsonEncodeTest extends TestCase
{
private $encoder;
protected function setUp()
{
$this->encode = new JsonEncode();
}
public function testSupportsEncoding()
{
$this->assertTrue($this->encode->supportsEncoding(JsonEncoder::FORMAT));
$this->assertFalse($this->encode->supportsEncoding('foobar'));
}
/**
* @dataProvider encodeProvider
*/
public function testEncode($toEncode, $expected, $context)
{
$this->assertEquals(
$expected,
$this->encode->encode($toEncode, JsonEncoder::FORMAT, $context)
);
}
public function encodeProvider()
{
return [
[[], '[]', []],
[[], '{}', ['json_encode_options' => \JSON_FORCE_OBJECT]],
];
}
/**
* @requires function json_last_error_msg
*/
public function testEncodeWithError()
{
$this->expectException('Symfony\Component\Serializer\Exception\UnexpectedValueException');
$this->encode->encode("\xB1\x31", JsonEncoder::FORMAT);
}
}
@@ -1,119 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Encoder;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\CustomNormalizer;
use Symfony\Component\Serializer\Serializer;
class JsonEncoderTest extends TestCase
{
private $encoder;
private $serializer;
protected function setUp()
{
$this->encoder = new JsonEncoder();
$this->serializer = new Serializer([new CustomNormalizer()], ['json' => new JsonEncoder()]);
}
public function testEncodeScalar()
{
$obj = new \stdClass();
$obj->foo = 'foo';
$expected = '{"foo":"foo"}';
$this->assertEquals($expected, $this->encoder->encode($obj, 'json'));
}
public function testComplexObject()
{
$obj = $this->getObject();
$expected = $this->getJsonSource();
$this->assertEquals($expected, $this->encoder->encode($obj, 'json'));
}
public function testOptions()
{
$context = ['json_encode_options' => \JSON_NUMERIC_CHECK];
$arr = [];
$arr['foo'] = '3';
$expected = '{"foo":3}';
$this->assertEquals($expected, $this->serializer->serialize($arr, 'json', $context));
$arr = [];
$arr['foo'] = '3';
$expected = '{"foo":"3"}';
$this->assertEquals($expected, $this->serializer->serialize($arr, 'json'), 'Context should not be persistent');
}
public function testEncodeNotUtf8WithoutPartialOnError()
{
$this->expectException('Symfony\Component\Serializer\Exception\UnexpectedValueException');
$arr = [
'utf8' => 'Hello World!',
'notUtf8' => "\xb0\xd0\xb5\xd0",
];
$this->encoder->encode($arr, 'json');
}
public function testEncodeNotUtf8WithPartialOnError()
{
$context = ['json_encode_options' => \JSON_PARTIAL_OUTPUT_ON_ERROR];
$arr = [
'utf8' => 'Hello World!',
'notUtf8' => "\xb0\xd0\xb5\xd0",
];
$result = $this->encoder->encode($arr, 'json', $context);
$jsonLastError = json_last_error();
$this->assertSame(\JSON_ERROR_UTF8, $jsonLastError);
$this->assertEquals('{"utf8":"Hello World!","notUtf8":null}', $result);
$this->assertEquals('0', $this->serializer->serialize(\NAN, 'json', $context));
}
public function testDecodeFalseString()
{
$result = $this->encoder->decode('false', 'json');
$this->assertSame(\JSON_ERROR_NONE, json_last_error());
$this->assertFalse($result);
}
protected function getJsonSource()
{
return '{"foo":"foo","bar":["a","b"],"baz":{"key":"val","key2":"val","A B":"bar","item":[{"title":"title1"},{"title":"title2"}],"Barry":{"FooBar":{"Baz":"Ed","@id":1}}},"qux":"1"}';
}
protected function getObject()
{
$obj = new \stdClass();
$obj->foo = 'foo';
$obj->bar = ['a', 'b'];
$obj->baz = ['key' => 'val', 'key2' => 'val', 'A B' => 'bar', 'item' => [['title' => 'title1'], ['title' => 'title2']], 'Barry' => ['FooBar' => ['Baz' => 'Ed', '@id' => 1]]];
$obj->qux = '1';
return $obj;
}
}
@@ -1,768 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Encoder;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Exception\NotEncodableValueException;
use Symfony\Component\Serializer\Normalizer\CustomNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Tests\Fixtures\Dummy;
use Symfony\Component\Serializer\Tests\Fixtures\NormalizableTraversableDummy;
use Symfony\Component\Serializer\Tests\Fixtures\ScalarDummy;
class XmlEncoderTest extends TestCase
{
/**
* @var XmlEncoder
*/
private $encoder;
private $exampleDateTimeString = '2017-02-19T15:16:08+0300';
protected function setUp()
{
$this->encoder = new XmlEncoder();
$serializer = new Serializer([new CustomNormalizer()], ['xml' => new XmlEncoder()]);
$this->encoder->setSerializer($serializer);
}
public function testEncodeScalar()
{
$obj = new ScalarDummy();
$obj->xmlFoo = 'foo';
$expected = '<?xml version="1.0"?>'."\n".
'<response>foo</response>'."\n";
$this->assertEquals($expected, $this->encoder->encode($obj, 'xml'));
}
public function testSetRootNodeName()
{
$obj = new ScalarDummy();
$obj->xmlFoo = 'foo';
$this->encoder->setRootNodeName('test');
$expected = '<?xml version="1.0"?>'."\n".
'<test>foo</test>'."\n";
$this->assertEquals($expected, $this->encoder->encode($obj, 'xml'));
}
public function testDocTypeIsNotAllowed()
{
$this->expectException('Symfony\Component\Serializer\Exception\UnexpectedValueException');
$this->expectExceptionMessage('Document types are not allowed.');
$this->encoder->decode('<?xml version="1.0"?><!DOCTYPE foo><foo></foo>', 'foo');
}
public function testAttributes()
{
$obj = new ScalarDummy();
$obj->xmlFoo = [
'foo-bar' => [
'@id' => 1,
'@name' => 'Bar',
],
'Foo' => [
'Bar' => 'Test',
'@Type' => 'test',
],
'föo_bär' => 'a',
'Bar' => [1, 2, 3],
'a' => 'b',
];
$expected = '<?xml version="1.0"?>'."\n".
'<response>'.
'<foo-bar id="1" name="Bar"/>'.
'<Foo Type="test"><Bar>Test</Bar></Foo>'.
'<föo_bär>a</föo_bär>'.
'<Bar>1</Bar>'.
'<Bar>2</Bar>'.
'<Bar>3</Bar>'.
'<a>b</a>'.
'</response>'."\n";
$this->assertEquals($expected, $this->encoder->encode($obj, 'xml'));
}
public function testElementNameValid()
{
$obj = new ScalarDummy();
$obj->xmlFoo = [
'foo-bar' => 'a',
'foo_bar' => 'a',
'föo_bär' => 'a',
];
$expected = '<?xml version="1.0"?>'."\n".
'<response>'.
'<foo-bar>a</foo-bar>'.
'<foo_bar>a</foo_bar>'.
'<föo_bär>a</föo_bär>'.
'</response>'."\n";
$this->assertEquals($expected, $this->encoder->encode($obj, 'xml'));
}
public function testEncodeSimpleXML()
{
$xml = simplexml_load_string('<firstname>Peter</firstname>');
$array = ['person' => $xml];
$expected = '<?xml version="1.0"?>'."\n".
'<response><person><firstname>Peter</firstname></person></response>'."\n";
$this->assertEquals($expected, $this->encoder->encode($array, 'xml'));
}
public function testEncodeXmlAttributes()
{
$xml = simplexml_load_string('<firstname>Peter</firstname>');
$array = ['person' => $xml];
$expected = '<?xml version="1.1" encoding="utf-8" standalone="yes"?>'."\n".
'<response><person><firstname>Peter</firstname></person></response>'."\n";
$context = [
'xml_version' => '1.1',
'xml_encoding' => 'utf-8',
'xml_standalone' => true,
];
$this->assertSame($expected, $this->encoder->encode($array, 'xml', $context));
}
public function testEncodeRemovingEmptyTags()
{
$array = ['person' => ['firstname' => 'Peter', 'lastname' => null]];
$expected = '<?xml version="1.0"?>'."\n".
'<response><person><firstname>Peter</firstname></person></response>'."\n";
$context = ['remove_empty_tags' => true];
$this->assertSame($expected, $this->encoder->encode($array, 'xml', $context));
}
public function testEncodeNotRemovingEmptyTags()
{
$array = ['person' => ['firstname' => 'Peter', 'lastname' => null]];
$expected = '<?xml version="1.0"?>'."\n".
'<response><person><firstname>Peter</firstname><lastname/></person></response>'."\n";
$this->assertSame($expected, $this->encoder->encode($array, 'xml'));
}
public function testContext()
{
$array = ['person' => ['name' => 'George Abitbol']];
$expected = <<<'XML'
<?xml version="1.0"?>
<response>
<person>
<name>George Abitbol</name>
</person>
</response>
XML;
$context = [
'xml_format_output' => true,
];
$this->assertSame($expected, $this->encoder->encode($array, 'xml', $context));
}
public function testEncodeScalarRootAttributes()
{
$array = [
'#' => 'Paul',
'@eye-color' => 'brown',
];
$expected = '<?xml version="1.0"?>'."\n".
'<response eye-color="brown">Paul</response>'."\n";
$this->assertEquals($expected, $this->encoder->encode($array, 'xml'));
}
public function testEncodeRootAttributes()
{
$array = [
'firstname' => 'Paul',
'@eye-color' => 'brown',
];
$expected = '<?xml version="1.0"?>'."\n".
'<response eye-color="brown"><firstname>Paul</firstname></response>'."\n";
$this->assertEquals($expected, $this->encoder->encode($array, 'xml'));
}
public function testEncodeCdataWrapping()
{
$array = [
'firstname' => 'Paul <or Me>',
];
$expected = '<?xml version="1.0"?>'."\n".
'<response><firstname><![CDATA[Paul <or Me>]]></firstname></response>'."\n";
$this->assertEquals($expected, $this->encoder->encode($array, 'xml'));
}
public function testEncodeScalarWithAttribute()
{
$array = [
'person' => ['@eye-color' => 'brown', '#' => 'Peter'],
];
$expected = '<?xml version="1.0"?>'."\n".
'<response><person eye-color="brown">Peter</person></response>'."\n";
$this->assertEquals($expected, $this->encoder->encode($array, 'xml'));
}
public function testDecodeScalar()
{
$source = '<?xml version="1.0"?>'."\n".
'<response>foo</response>'."\n";
$this->assertEquals('foo', $this->encoder->decode($source, 'xml'));
}
public function testDecodeBigDigitAttributes()
{
$source = <<<XML
<?xml version="1.0"?>
<document index="182077241760011681341821060401202210011000045913000000017100">Name</document>
XML;
$this->assertSame(['@index' => 182077241760011681341821060401202210011000045913000000017100, '#' => 'Name'], $this->encoder->decode($source, 'xml'));
}
public function testDecodeNegativeIntAttribute()
{
$source = <<<XML
<?xml version="1.0"?>
<document index="-1234">Name</document>
XML;
$this->assertSame(['@index' => -1234, '#' => 'Name'], $this->encoder->decode($source, 'xml'));
}
public function testDecodeFloatAttribute()
{
$source = <<<XML
<?xml version="1.0"?>
<document index="12.11">Name</document>
XML;
$this->assertSame(['@index' => 12.11, '#' => 'Name'], $this->encoder->decode($source, 'xml'));
}
public function testDecodeNegativeFloatAttribute()
{
$source = <<<XML
<?xml version="1.0"?>
<document index="-12.11">Name</document>
XML;
$this->assertSame(['@index' => -12.11, '#' => 'Name'], $this->encoder->decode($source, 'xml'));
}
public function testDecodeFloatAttributeWithZeroWholeNumber()
{
$source = <<<XML
<?xml version="1.0"?>
<document index="0.123">Name</document>
XML;
$this->assertSame(['@index' => 0.123, '#' => 'Name'], $this->encoder->decode($source, 'xml'));
}
public function testNoTypeCastAttribute()
{
$source = <<<XML
<?xml version="1.0"?>
<document a="018" b="-12.11">
<node a="018" b="-12.11"/>
</document>
XML;
$data = $this->encoder->decode($source, 'xml', ['xml_type_cast_attributes' => false]);
$expected = [
'@a' => '018',
'@b' => '-12.11',
'node' => [
'@a' => '018',
'@b' => '-12.11',
'#' => '',
],
];
$this->assertSame($expected, $data);
}
public function testDoesNotTypeCastStringsStartingWith0()
{
$source = <<<XML
<?xml version="1.0"?>
<document a="018"></document>
XML;
$data = $this->encoder->decode($source, 'xml');
$this->assertSame('018', $data['@a']);
}
public function testEncode()
{
$source = $this->getXmlSource();
$obj = $this->getObject();
$this->assertEquals($source, $this->encoder->encode($obj, 'xml'));
}
public function testEncodeWithNamespace()
{
$source = $this->getNamespacedXmlSource();
$array = $this->getNamespacedArray();
$this->assertEquals($source, $this->encoder->encode($array, 'xml'));
}
public function testEncodeSerializerXmlRootNodeNameOption()
{
$options = ['xml_root_node_name' => 'test'];
$this->encoder = new XmlEncoder();
$serializer = new Serializer([], ['xml' => new XmlEncoder()]);
$this->encoder->setSerializer($serializer);
$array = [
'person' => ['@eye-color' => 'brown', '#' => 'Peter'],
];
$expected = '<?xml version="1.0"?>'."\n".
'<test><person eye-color="brown">Peter</person></test>'."\n";
$this->assertEquals($expected, $serializer->serialize($array, 'xml', $options));
}
public function testEncodeTraversableWhenNormalizable()
{
$this->encoder = new XmlEncoder();
$serializer = new Serializer([new CustomNormalizer()], ['xml' => new XmlEncoder()]);
$this->encoder->setSerializer($serializer);
$expected = <<<'XML'
<?xml version="1.0"?>
<response><foo>normalizedFoo</foo><bar>normalizedBar</bar></response>
XML;
$this->assertEquals($expected, $serializer->serialize(new NormalizableTraversableDummy(), 'xml'));
}
public function testDecode()
{
$source = $this->getXmlSource();
$obj = $this->getObject();
$this->assertEquals(get_object_vars($obj), $this->encoder->decode($source, 'xml'));
}
public function testDecodeCdataWrapping()
{
$expected = [
'firstname' => 'Paul <or Me>',
];
$xml = '<?xml version="1.0"?>'."\n".
'<response><firstname><![CDATA[Paul <or Me>]]></firstname></response>'."\n";
$this->assertEquals($expected, $this->encoder->decode($xml, 'xml'));
}
public function testDecodeCdataWrappingAndWhitespace()
{
$expected = [
'firstname' => 'Paul <or Me>',
];
$xml = '<?xml version="1.0"?>'."\n".
'<response><firstname>'."\n".
'<![CDATA[Paul <or Me>]]></firstname></response>'."\n";
$this->assertEquals($expected, $this->encoder->decode($xml, 'xml'));
}
public function testDecodeWithNamespace()
{
$source = $this->getNamespacedXmlSource();
$array = $this->getNamespacedArray();
$this->assertEquals($array, $this->encoder->decode($source, 'xml'));
}
public function testDecodeScalarWithAttribute()
{
$source = '<?xml version="1.0"?>'."\n".
'<response><person eye-color="brown">Peter</person></response>'."\n";
$expected = [
'person' => ['@eye-color' => 'brown', '#' => 'Peter'],
];
$this->assertEquals($expected, $this->encoder->decode($source, 'xml'));
}
public function testDecodeScalarRootAttributes()
{
$source = '<?xml version="1.0"?>'."\n".
'<person eye-color="brown">Peter</person>'."\n";
$expected = [
'#' => 'Peter',
'@eye-color' => 'brown',
];
$this->assertEquals($expected, $this->encoder->decode($source, 'xml'));
}
public function testDecodeRootAttributes()
{
$source = '<?xml version="1.0"?>'."\n".
'<person eye-color="brown"><firstname>Peter</firstname><lastname>Mac Calloway</lastname></person>'."\n";
$expected = [
'firstname' => 'Peter',
'lastname' => 'Mac Calloway',
'@eye-color' => 'brown',
];
$this->assertEquals($expected, $this->encoder->decode($source, 'xml'));
}
public function testDecodeArray()
{
$source = '<?xml version="1.0"?>'."\n".
'<response>'.
'<people>'.
'<person><firstname>Benjamin</firstname><lastname>Alexandre</lastname></person>'.
'<person><firstname>Damien</firstname><lastname>Clay</lastname></person>'.
'</people>'.
'</response>'."\n";
$expected = [
'people' => ['person' => [
['firstname' => 'Benjamin', 'lastname' => 'Alexandre'],
['firstname' => 'Damien', 'lastname' => 'Clay'],
]],
];
$this->assertEquals($expected, $this->encoder->decode($source, 'xml'));
}
public function testDecodeXMLWithProcessInstruction()
{
$source = <<<'XML'
<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="/xsl/xmlverbatimwrapper.xsl"?>
<?display table-view?>
<?sort alpha-ascending?>
<response>
<foo>foo</foo>
<?textinfo whitespace is allowed ?>
<bar>a</bar>
<bar>b</bar>
<baz>
<key>val</key>
<key2>val</key2>
<item key="A B">bar</item>
<item>
<title>title1</title>
</item>
<?item ignore-title ?>
<item>
<title>title2</title>
</item>
<Barry>
<FooBar id="1">
<Baz>Ed</Baz>
</FooBar>
</Barry>
</baz>
<qux>1</qux>
</response>
<?instruction <value> ?>
XML;
$obj = $this->getObject();
$this->assertEquals(get_object_vars($obj), $this->encoder->decode($source, 'xml'));
}
public function testDecodeIgnoreWhiteSpace()
{
$source = <<<'XML'
<?xml version="1.0"?>
<people>
<person>
<firstname>Benjamin</firstname>
<lastname>Alexandre</lastname>
</person>
<person>
<firstname>Damien</firstname>
<lastname>Clay</lastname>
</person>
</people>
XML;
$expected = ['person' => [
['firstname' => 'Benjamin', 'lastname' => 'Alexandre'],
['firstname' => 'Damien', 'lastname' => 'Clay'],
]];
$this->assertEquals($expected, $this->encoder->decode($source, 'xml'));
}
public function testDecodeWithoutItemHash()
{
$obj = new ScalarDummy();
$obj->xmlFoo = [
'foo-bar' => [
'@key' => 'value',
'item' => ['@key' => 'key', 'key-val' => 'val'],
],
'Foo' => [
'Bar' => 'Test',
'@Type' => 'test',
],
'föo_bär' => 'a',
'Bar' => [1, 2, 3],
'a' => 'b',
];
$expected = [
'foo-bar' => [
'@key' => 'value',
'key' => ['@key' => 'key', 'key-val' => 'val'],
],
'Foo' => [
'Bar' => 'Test',
'@Type' => 'test',
],
'föo_bär' => 'a',
'Bar' => [1, 2, 3],
'a' => 'b',
];
$xml = $this->encoder->encode($obj, 'xml');
$this->assertEquals($expected, $this->encoder->decode($xml, 'xml'));
}
public function testDecodeInvalidXml()
{
$this->expectException('Symfony\Component\Serializer\Exception\UnexpectedValueException');
$this->encoder->decode('<?xml version="1.0"?><invalid><xml>', 'xml');
}
public function testPreventsComplexExternalEntities()
{
$this->expectException('Symfony\Component\Serializer\Exception\UnexpectedValueException');
$this->encoder->decode('<?xml version="1.0"?><!DOCTYPE scan[<!ENTITY test SYSTEM "php://filter/read=convert.base64-encode/resource=XmlEncoderTest.php">]><scan>&test;</scan>', 'xml');
}
public function testDecodeEmptyXml()
{
$this->expectException('Symfony\Component\Serializer\Exception\UnexpectedValueException');
$this->expectExceptionMessage('Invalid XML data, it can not be empty.');
$this->encoder->decode(' ', 'xml');
}
protected function getXmlSource()
{
return '<?xml version="1.0"?>'."\n".
'<response>'.
'<foo>foo</foo>'.
'<bar>a</bar><bar>b</bar>'.
'<baz><key>val</key><key2>val</key2><item key="A B">bar</item>'.
'<item><title>title1</title></item><item><title>title2</title></item>'.
'<Barry><FooBar id="1"><Baz>Ed</Baz></FooBar></Barry></baz>'.
'<qux>1</qux>'.
'</response>'."\n";
}
protected function getNamespacedXmlSource()
{
return '<?xml version="1.0"?>'."\n".
'<response xmlns="http://www.w3.org/2005/Atom" xmlns:app="http://www.w3.org/2007/app" xmlns:media="http://search.yahoo.com/mrss/" xmlns:gd="http://schemas.google.com/g/2005" xmlns:yt="http://gdata.youtube.com/schemas/2007">'.
'<qux>1</qux>'.
'<app:foo>foo</app:foo>'.
'<yt:bar>a</yt:bar><yt:bar>b</yt:bar>'.
'<media:baz><media:key>val</media:key><media:key2>val</media:key2><item key="A B">bar</item>'.
'<item><title>title1</title></item><item><title>title2</title></item>'.
'<Barry size="large"><FooBar gd:id="1"><Baz>Ed</Baz></FooBar></Barry></media:baz>'.
'</response>'."\n";
}
protected function getNamespacedArray()
{
return [
'@xmlns' => 'http://www.w3.org/2005/Atom',
'@xmlns:app' => 'http://www.w3.org/2007/app',
'@xmlns:media' => 'http://search.yahoo.com/mrss/',
'@xmlns:gd' => 'http://schemas.google.com/g/2005',
'@xmlns:yt' => 'http://gdata.youtube.com/schemas/2007',
'qux' => '1',
'app:foo' => 'foo',
'yt:bar' => ['a', 'b'],
'media:baz' => [
'media:key' => 'val',
'media:key2' => 'val',
'A B' => 'bar',
'item' => [
[
'title' => 'title1',
],
[
'title' => 'title2',
],
],
'Barry' => [
'@size' => 'large',
'FooBar' => [
'Baz' => 'Ed',
'@gd:id' => 1,
],
],
],
];
}
protected function getObject()
{
$obj = new Dummy();
$obj->foo = 'foo';
$obj->bar = ['a', 'b'];
$obj->baz = ['key' => 'val', 'key2' => 'val', 'A B' => 'bar', 'item' => [['title' => 'title1'], ['title' => 'title2']], 'Barry' => ['FooBar' => ['Baz' => 'Ed', '@id' => 1]]];
$obj->qux = '1';
return $obj;
}
public function testEncodeXmlWithBoolValue()
{
$expectedXml = <<<'XML'
<?xml version="1.0"?>
<response><foo>1</foo><bar>0</bar></response>
XML;
$actualXml = $this->encoder->encode(['foo' => true, 'bar' => false], 'xml');
$this->assertEquals($expectedXml, $actualXml);
}
public function testEncodeXmlWithDomNodeValue()
{
$expectedXml = <<<'XML'
<?xml version="1.0"?>
<response><foo>bar</foo><bar>foo &amp; bar</bar></response>
XML;
$document = new \DOMDocument();
$actualXml = $this->encoder->encode(['foo' => $document->createTextNode('bar'), 'bar' => $document->createTextNode('foo & bar')], 'xml');
$this->assertEquals($expectedXml, $actualXml);
}
public function testEncodeXmlWithDateTimeObjectValue()
{
$xmlEncoder = $this->createXmlEncoderWithDateTimeNormalizer();
$actualXml = $xmlEncoder->encode(['dateTime' => new \DateTime($this->exampleDateTimeString)], 'xml');
$this->assertEquals($this->createXmlWithDateTime(), $actualXml);
}
public function testEncodeXmlWithDateTimeObjectField()
{
$xmlEncoder = $this->createXmlEncoderWithDateTimeNormalizer();
$actualXml = $xmlEncoder->encode(['foo' => ['@dateTime' => new \DateTime($this->exampleDateTimeString)]], 'xml');
$this->assertEquals($this->createXmlWithDateTimeField(), $actualXml);
}
public function testNotEncodableValueExceptionMessageForAResource()
{
$this->expectException(NotEncodableValueException::class);
$this->expectExceptionMessage('An unexpected value could not be serialized: stream resource');
(new XmlEncoder())->encode(tmpfile(), 'xml');
}
/**
* @return XmlEncoder
*/
private function createXmlEncoderWithDateTimeNormalizer()
{
$encoder = new XmlEncoder();
$serializer = new Serializer([$this->createMockDateTimeNormalizer()], ['xml' => new XmlEncoder()]);
$encoder->setSerializer($serializer);
return $encoder;
}
/**
* @return MockObject|NormalizerInterface
*/
private function createMockDateTimeNormalizer()
{
$mock = $this->getMockBuilder('\Symfony\Component\Serializer\Normalizer\CustomNormalizer')->getMock();
$mock
->expects($this->once())
->method('normalize')
->with(new \DateTime($this->exampleDateTimeString), 'xml', [])
->willReturn($this->exampleDateTimeString);
$mock
->expects($this->once())
->method('supportsNormalization')
->with(new \DateTime($this->exampleDateTimeString), 'xml')
->willReturn(true);
return $mock;
}
/**
* @return string
*/
private function createXmlWithDateTime()
{
return sprintf('<?xml version="1.0"?>
<response><dateTime>%s</dateTime></response>
', $this->exampleDateTimeString);
}
/**
* @return string
*/
private function createXmlWithDateTimeField()
{
return sprintf('<?xml version="1.0"?>
<response><foo dateTime="%s"/></response>
', $this->exampleDateTimeString);
}
}
@@ -1,71 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Encoder;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Encoder\YamlEncoder;
use Symfony\Component\Yaml\Dumper;
use Symfony\Component\Yaml\Parser;
use Symfony\Component\Yaml\Yaml;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class YamlEncoderTest extends TestCase
{
public function testEncode()
{
$encoder = new YamlEncoder();
$this->assertEquals('foo', $encoder->encode('foo', 'yaml'));
$this->assertEquals('{ foo: 1 }', $encoder->encode(['foo' => 1], 'yaml'));
}
public function testSupportsEncoding()
{
$encoder = new YamlEncoder();
$this->assertTrue($encoder->supportsEncoding('yaml'));
$this->assertFalse($encoder->supportsEncoding('json'));
}
public function testDecode()
{
$encoder = new YamlEncoder();
$this->assertEquals('foo', $encoder->decode('foo', 'yaml'));
$this->assertEquals(['foo' => 1], $encoder->decode('{ foo: 1 }', 'yaml'));
}
public function testSupportsDecoding()
{
$encoder = new YamlEncoder();
$this->assertTrue($encoder->supportsDecoding('yaml'));
$this->assertFalse($encoder->supportsDecoding('json'));
}
public function testContext()
{
$encoder = new YamlEncoder(new Dumper(), new Parser(), ['yaml_inline' => 1, 'yaml_indent' => 4, 'yaml_flags' => Yaml::DUMP_OBJECT | Yaml::PARSE_OBJECT]);
$obj = new \stdClass();
$obj->bar = 2;
$legacyTag = " foo: !php/object:O:8:\"stdClass\":1:{s:3:\"bar\";i:2;}\n";
$spacedTag = " foo: !php/object 'O:8:\"stdClass\":1:{s:3:\"bar\";i:2;}'\n";
$this->assertThat($encoder->encode(['foo' => $obj], 'yaml'), $this->logicalOr($this->equalTo($legacyTag), $this->equalTo($spacedTag)));
$this->assertEquals(' { foo: null }', $encoder->encode(['foo' => $obj], 'yaml', ['yaml_inline' => 0, 'yaml_indent' => 2, 'yaml_flags' => 0]));
$this->assertEquals(['foo' => $obj], $encoder->decode("foo: !php/object 'O:8:\"stdClass\":1:{s:3:\"bar\";i:2;}'", 'yaml'));
$this->assertEquals(['foo' => null], $encoder->decode("foo: !php/object 'O:8:\"stdClass\":1:{s:3:\"bar\";i:2;}'", 'yaml', ['yaml_flags' => 0]));
}
}
@@ -1,60 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
/**
* Provides a dummy Normalizer which extends the AbstractNormalizer.
*
* @author Konstantin S. M. Möllers <ksm.moellers@gmail.com>
*/
class AbstractNormalizerDummy extends AbstractNormalizer
{
/**
* {@inheritdoc}
*/
public function getAllowedAttributes($classOrObject, array $context, $attributesAsString = false)
{
return parent::getAllowedAttributes($classOrObject, $context, $attributesAsString);
}
/**
* {@inheritdoc}
*/
public function normalize($object, $format = null, array $context = [])
{
}
/**
* {@inheritdoc}
*/
public function supportsNormalization($data, $format = null)
{
return true;
}
/**
* {@inheritdoc}
*/
public function denormalize($data, $type, $format = null, array $context = [])
{
}
/**
* {@inheritdoc}
*/
public function supportsDenormalization($data, $type, $format = null)
{
return true;
}
}
@@ -1,23 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class CircularReferenceDummy
{
public function getMe()
{
return $this;
}
}
@@ -1,22 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures;
use Symfony\Component\Serializer\Normalizer\DenormalizableInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
class DenormalizableDummy implements DenormalizableInterface
{
public function denormalize(DenormalizerInterface $denormalizer, $data, $format = null, array $context = [])
{
}
}
-43
View File
@@ -1,43 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures;
use Symfony\Component\Serializer\Normalizer\DenormalizableInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizableInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class Dummy implements NormalizableInterface, DenormalizableInterface
{
public $foo;
public $bar;
public $baz;
public $qux;
public function normalize(NormalizerInterface $normalizer, $format = null, array $context = [])
{
return [
'foo' => $this->foo,
'bar' => $this->bar,
'baz' => $this->baz,
'qux' => $this->qux,
];
}
public function denormalize(DenormalizerInterface $denormalizer, $data, $format = null, array $context = [])
{
$this->foo = $data['foo'];
$this->bar = $data['bar'];
$this->baz = $data['baz'];
$this->qux = $data['qux'];
}
}
-80
View File
@@ -1,80 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class GroupDummy extends GroupDummyParent implements GroupDummyInterface
{
/**
* @Groups({"a"})
*/
private $foo;
/**
* @Groups({"b", "c", "name_converter"})
*/
protected $bar;
private $fooBar;
private $symfony;
/**
* @Groups({"b"})
*/
public function setBar($bar)
{
$this->bar = $bar;
}
/**
* @Groups({"c"})
*/
public function getBar()
{
return $this->bar;
}
public function setFoo($foo)
{
$this->foo = $foo;
}
public function getFoo()
{
return $this->foo;
}
public function setFooBar($fooBar)
{
$this->fooBar = $fooBar;
}
/**
* @Groups({"a", "b", "name_converter"})
*/
public function isFooBar()
{
return $this->fooBar;
}
public function setSymfony($symfony)
{
$this->symfony = $symfony;
}
public function getSymfony()
{
return $this->symfony;
}
}
@@ -1,33 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures;
class GroupDummyChild extends GroupDummy
{
private $baz;
/**
* @return mixed
*/
public function getBaz()
{
return $this->baz;
}
/**
* @param mixed $baz
*/
public function setBaz($baz)
{
$this->baz = $baz;
}
}
@@ -1,25 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
interface GroupDummyInterface
{
/**
* @Groups({"a", "name_converter"})
*/
public function getSymfony();
}
@@ -1,49 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class GroupDummyParent
{
/**
* @Groups({"a"})
*/
private $kevin;
private $coopTilleuls;
public function setKevin($kevin)
{
$this->kevin = $kevin;
}
public function getKevin()
{
return $this->kevin;
}
public function setCoopTilleuls($coopTilleuls)
{
$this->coopTilleuls = $coopTilleuls;
}
/**
* @Groups({"a", "b"})
*/
public function getCoopTilleuls()
{
return $this->coopTilleuls;
}
}
@@ -1,25 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures;
class JsonSerializableDummy implements \JsonSerializable
{
public function jsonSerialize()
{
return [
'foo' => 'a',
'bar' => 'b',
'baz' => 'c',
'qux' => $this,
];
}
}
@@ -1,45 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures;
use Symfony\Component\Serializer\Annotation\MaxDepth;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class MaxDepthDummy
{
/**
* @MaxDepth(2)
*/
public $foo;
public $bar;
/**
* @var self
*/
public $child;
/**
* @MaxDepth(3)
*/
public function getBar()
{
return $this->bar;
}
public function getChild()
{
return $this->child;
}
}
@@ -1,36 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures;
use Symfony\Component\Serializer\Normalizer\DenormalizableInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizableInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class NormalizableTraversableDummy extends TraversableDummy implements NormalizableInterface, DenormalizableInterface
{
public function normalize(NormalizerInterface $normalizer, $format = null, array $context = [])
{
return [
'foo' => 'normalizedFoo',
'bar' => 'normalizedBar',
];
}
public function denormalize(DenormalizerInterface $denormalizer, $data, $format = null, array $context = [])
{
return [
'foo' => 'denormalizedFoo',
'bar' => 'denormalizedBar',
];
}
}
@@ -1,32 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures;
class NullableConstructorArgumentDummy
{
private $foo;
public function __construct(?\stdClass $foo)
{
$this->foo = $foo;
}
public function setFoo($foo)
{
$this->foo = 'this setter should not be called when using the constructor argument';
}
public function getFoo()
{
return $this->foo;
}
}
-22
View File
@@ -1,22 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures;
/**
* @author Valentin Udaltsov <udaltsov.valentin@gmail.com>
*/
final class Php74Dummy
{
public string $uninitializedProperty;
public string $initializedProperty = 'defaultValue';
}
@@ -1,25 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class PropertyCircularReferenceDummy
{
public $me;
public function __construct()
{
$this->me = $this;
}
}
@@ -1,39 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class PropertySiblingHolder
{
public $sibling0;
public $sibling1;
public $sibling2;
public function __construct()
{
$sibling = new PropertySibling();
$this->sibling0 = $sibling;
$this->sibling1 = $sibling;
$this->sibling2 = $sibling;
}
}
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class PropertySibling
{
public $coopTilleuls = 'Les-Tilleuls.coop';
}
-19
View File
@@ -1,19 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class ProxyDummy extends ToBeProxyfiedDummy
{
}
@@ -1,37 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures;
use Symfony\Component\Serializer\Normalizer\DenormalizableInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizableInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class ScalarDummy implements NormalizableInterface, DenormalizableInterface
{
public $foo;
public $xmlFoo;
public function normalize(NormalizerInterface $normalizer, $format = null, array $context = [])
{
return 'xml' === $format ? $this->xmlFoo : $this->foo;
}
public function denormalize(DenormalizerInterface $denormalizer, $data, $format = null, array $context = [])
{
if ('xml' === $format) {
$this->xmlFoo = $data;
} else {
$this->foo = $data;
}
}
}
@@ -1,57 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class SiblingHolder
{
private $sibling0;
private $sibling1;
private $sibling2;
public function __construct()
{
$sibling = new Sibling();
$this->sibling0 = $sibling;
$this->sibling1 = $sibling;
$this->sibling2 = $sibling;
}
public function getSibling0()
{
return $this->sibling0;
}
public function getSibling1()
{
return $this->sibling1;
}
public function getSibling2()
{
return $this->sibling2;
}
}
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class Sibling
{
public function getCoopTilleuls()
{
return 'Les-Tilleuls.coop';
}
}
@@ -1,31 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures;
class StaticConstructorDummy
{
public $foo;
public $bar;
public $quz;
public static function create($foo)
{
$dummy = new self();
$dummy->quz = $foo;
return $dummy;
}
private function __construct()
{
}
}
@@ -1,32 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
/**
* @author Guilhem N. <egetick@gmail.com>
*/
class StaticConstructorNormalizer extends ObjectNormalizer
{
/**
* {@inheritdoc}
*/
protected function getConstructor(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes)
{
if (is_a($class, StaticConstructorDummy::class, true)) {
return new \ReflectionMethod($class, 'create');
}
return parent::getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes);
}
}
@@ -1,30 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class ToBeProxyfiedDummy
{
private $foo;
public function setFoo($foo)
{
$this->foo = $foo;
}
public function getFoo()
{
return $this->foo;
}
}
@@ -1,23 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures;
class TraversableDummy implements \IteratorAggregate
{
public $foo = 'foo';
public $bar = 'bar';
public function getIterator()
{
return new \ArrayIterator(get_object_vars($this));
}
}
@@ -1,27 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures;
class VariadicConstructorArgsDummy
{
private $foo;
public function __construct(...$foo)
{
$this->foo = $foo;
}
public function getFoo()
{
return $this->foo;
}
}
@@ -1,28 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures;
class VariadicConstructorTypedArgsDummy
{
private $foo;
public function __construct(Dummy ...$foo)
{
$this->foo = $foo;
}
/** @return Dummy[] */
public function getFoo()
{
return $this->foo;
}
}
@@ -1 +0,0 @@
foo
@@ -1,23 +0,0 @@
<?xml version="1.0" ?>
<serializer xmlns="http://symfony.com/schema/dic/serializer-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd">
<class name="Symfony\Component\Serializer\Tests\Fixtures\GroupDummy">
<attribute name="foo">
<group>group1</group>
<group>group2</group>
</attribute>
<attribute name="bar">
<group>group2</group>
</attribute>
</class>
<class name="Symfony\Component\Serializer\Tests\Fixtures\MaxDepthDummy">
<attribute name="foo" max-depth="2" />
<attribute name="bar" max-depth="3" />
</class>
</serializer>
@@ -1,12 +0,0 @@
'Symfony\Component\Serializer\Tests\Fixtures\GroupDummy':
attributes:
foo:
groups: ['group1', 'group2']
bar:
groups: ['group2']
'Symfony\Component\Serializer\Tests\Fixtures\MaxDepthDummy':
attributes:
foo:
max_depth: 2
bar:
max_depth: 3
Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 B

-1
View File
@@ -1 +0,0 @@
Kévin Dunglas
@@ -1,79 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Mapping;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class AttributeMetadataTest extends TestCase
{
public function testInterface()
{
$attributeMetadata = new AttributeMetadata('name');
$this->assertInstanceOf('Symfony\Component\Serializer\Mapping\AttributeMetadataInterface', $attributeMetadata);
}
public function testGetName()
{
$attributeMetadata = new AttributeMetadata('name');
$this->assertEquals('name', $attributeMetadata->getName());
}
public function testGroups()
{
$attributeMetadata = new AttributeMetadata('group');
$attributeMetadata->addGroup('a');
$attributeMetadata->addGroup('a');
$attributeMetadata->addGroup('b');
$this->assertEquals(['a', 'b'], $attributeMetadata->getGroups());
}
public function testMaxDepth()
{
$attributeMetadata = new AttributeMetadata('name');
$attributeMetadata->setMaxDepth(69);
$this->assertEquals(69, $attributeMetadata->getMaxDepth());
}
public function testMerge()
{
$attributeMetadata1 = new AttributeMetadata('a1');
$attributeMetadata1->addGroup('a');
$attributeMetadata1->addGroup('b');
$attributeMetadata2 = new AttributeMetadata('a2');
$attributeMetadata2->addGroup('a');
$attributeMetadata2->addGroup('c');
$attributeMetadata2->setMaxDepth(2);
$attributeMetadata1->merge($attributeMetadata2);
$this->assertEquals(['a', 'b', 'c'], $attributeMetadata1->getGroups());
$this->assertEquals(2, $attributeMetadata1->getMaxDepth());
}
public function testSerialize()
{
$attributeMetadata = new AttributeMetadata('attribute');
$attributeMetadata->addGroup('a');
$attributeMetadata->addGroup('b');
$attributeMetadata->setMaxDepth(3);
$serialized = serialize($attributeMetadata);
$this->assertEquals($attributeMetadata, unserialize($serialized));
}
}
@@ -1,76 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Mapping;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
use Symfony\Component\Serializer\Mapping\ClassMetadata;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class ClassMetadataTest extends TestCase
{
public function testInterface()
{
$classMetadata = new ClassMetadata('name');
$this->assertInstanceOf('Symfony\Component\Serializer\Mapping\ClassMetadataInterface', $classMetadata);
}
public function testAttributeMetadata()
{
$classMetadata = new ClassMetadata('c');
$a1 = new AttributeMetadata('a1');
$a2 = new AttributeMetadata('a2');
$classMetadata->addAttributeMetadata($a1);
$classMetadata->addAttributeMetadata($a2);
$this->assertEquals(['a1' => $a1, 'a2' => $a2], $classMetadata->getAttributesMetadata());
}
public function testMerge()
{
$classMetadata1 = new ClassMetadata('c1');
$classMetadata2 = new ClassMetadata('c2');
$ac1 = new AttributeMetadata('a1');
$ac1->addGroup('a');
$ac1->addGroup('b');
$ac2 = new AttributeMetadata('a1');
$ac2->addGroup('b');
$ac2->addGroup('c');
$classMetadata1->addAttributeMetadata($ac1);
$classMetadata2->addAttributeMetadata($ac2);
$classMetadata1->merge($classMetadata2);
$this->assertSame(['a', 'b', 'c'], $ac1->getGroups());
$this->assertEquals(['a1' => $ac1], $classMetadata1->getAttributesMetadata());
}
public function testSerialize()
{
$classMetadata = new ClassMetadata('a');
$a1 = new AttributeMetadata('b1');
$a2 = new AttributeMetadata('b2');
$classMetadata->addAttributeMetadata($a1);
$classMetadata->addAttributeMetadata($a2);
$serialized = serialize($classMetadata);
$this->assertEquals($classMetadata, unserialize($serialized));
}
}
@@ -1,66 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Mapping\Factory;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Serializer\Mapping\ClassMetadata;
use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\Tests\Fixtures\Dummy;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class CacheMetadataFactoryTest extends TestCase
{
public function testGetMetadataFor()
{
$metadata = new ClassMetadata(Dummy::class);
$decorated = $this->getMockBuilder(ClassMetadataFactoryInterface::class)->getMock();
$decorated
->expects($this->once())
->method('getMetadataFor')
->willReturn($metadata)
;
$factory = new CacheClassMetadataFactory($decorated, new ArrayAdapter());
$this->assertEquals($metadata, $factory->getMetadataFor(Dummy::class));
// The second call should retrieve the value from the cache
$this->assertEquals($metadata, $factory->getMetadataFor(Dummy::class));
}
public function testHasMetadataFor()
{
$decorated = $this->getMockBuilder(ClassMetadataFactoryInterface::class)->getMock();
$decorated
->expects($this->once())
->method('hasMetadataFor')
->willReturn(true)
;
$factory = new CacheClassMetadataFactory($decorated, new ArrayAdapter());
$this->assertTrue($factory->hasMetadataFor(Dummy::class));
}
public function testInvalidClassThrowsException()
{
$this->expectException('Symfony\Component\Serializer\Exception\InvalidArgumentException');
$decorated = $this->getMockBuilder(ClassMetadataFactoryInterface::class)->getMock();
$factory = new CacheClassMetadataFactory($decorated, new ArrayAdapter());
$factory->getMetadataFor('Not\Exist');
}
}
@@ -1,79 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Mapping\Factory;
use Doctrine\Common\Annotations\AnnotationReader;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
use Symfony\Component\Serializer\Mapping\Loader\LoaderChain;
use Symfony\Component\Serializer\Tests\Mapping\TestClassMetadataFactory;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class ClassMetadataFactoryTest extends TestCase
{
public function testInterface()
{
$classMetadata = new ClassMetadataFactory(new LoaderChain([]));
$this->assertInstanceOf('Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface', $classMetadata);
}
public function testGetMetadataFor()
{
$factory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$classMetadata = $factory->getMetadataFor('Symfony\Component\Serializer\Tests\Fixtures\GroupDummy');
$this->assertEquals(TestClassMetadataFactory::createClassMetadata(true, true), $classMetadata);
}
public function testHasMetadataFor()
{
$factory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$this->assertTrue($factory->hasMetadataFor('Symfony\Component\Serializer\Tests\Fixtures\GroupDummy'));
$this->assertTrue($factory->hasMetadataFor('Symfony\Component\Serializer\Tests\Fixtures\GroupDummyParent'));
$this->assertTrue($factory->hasMetadataFor('Symfony\Component\Serializer\Tests\Fixtures\GroupDummyInterface'));
$this->assertFalse($factory->hasMetadataFor('Dunglas\Entity'));
}
/**
* @group legacy
*/
public function testCacheExists()
{
$cache = $this->getMockBuilder('Doctrine\Common\Cache\Cache')->getMock();
$cache
->expects($this->once())
->method('fetch')
->willReturn('foo')
;
$factory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()), $cache);
$this->assertEquals('foo', $factory->getMetadataFor('Symfony\Component\Serializer\Tests\Fixtures\GroupDummy'));
}
/**
* @group legacy
*/
public function testCacheNotExists()
{
$cache = $this->getMockBuilder('Doctrine\Common\Cache\Cache')->getMock();
$cache->method('fetch')->willReturn(false);
$cache->method('save');
$factory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()), $cache);
$metadata = $factory->getMetadataFor('Symfony\Component\Serializer\Tests\Fixtures\GroupDummy');
$this->assertEquals(TestClassMetadataFactory::createClassMetadata(true, true), $metadata);
}
}
@@ -1,77 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Mapping\Loader;
use Doctrine\Common\Annotations\AnnotationReader;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Mapping\ClassMetadata;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
use Symfony\Component\Serializer\Tests\Mapping\TestClassMetadataFactory;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class AnnotationLoaderTest extends TestCase
{
/**
* @var AnnotationLoader
*/
private $loader;
protected function setUp()
{
$this->loader = new AnnotationLoader(new AnnotationReader());
}
public function testInterface()
{
$this->assertInstanceOf('Symfony\Component\Serializer\Mapping\Loader\LoaderInterface', $this->loader);
}
public function testLoadClassMetadataReturnsTrueIfSuccessful()
{
$classMetadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\GroupDummy');
$this->assertTrue($this->loader->loadClassMetadata($classMetadata));
}
public function testLoadGroups()
{
$classMetadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\GroupDummy');
$this->loader->loadClassMetadata($classMetadata);
$this->assertEquals(TestClassMetadataFactory::createClassMetadata(), $classMetadata);
}
public function testLoadMaxDepth()
{
$classMetadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\MaxDepthDummy');
$this->loader->loadClassMetadata($classMetadata);
$attributesMetadata = $classMetadata->getAttributesMetadata();
$this->assertEquals(2, $attributesMetadata['foo']->getMaxDepth());
$this->assertEquals(3, $attributesMetadata['bar']->getMaxDepth());
}
public function testLoadClassMetadataAndMerge()
{
$classMetadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\GroupDummy');
$parentClassMetadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\GroupDummyParent');
$this->loader->loadClassMetadata($parentClassMetadata);
$classMetadata->merge($parentClassMetadata);
$this->loader->loadClassMetadata($classMetadata);
$this->assertEquals(TestClassMetadataFactory::createClassMetadata(true), $classMetadata);
}
}
@@ -1,65 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Mapping\Loader;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Mapping\ClassMetadata;
use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader;
use Symfony\Component\Serializer\Tests\Mapping\TestClassMetadataFactory;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class XmlFileLoaderTest extends TestCase
{
/**
* @var XmlFileLoader
*/
private $loader;
/**
* @var ClassMetadata
*/
private $metadata;
protected function setUp()
{
$this->loader = new XmlFileLoader(__DIR__.'/../../Fixtures/serialization.xml');
$this->metadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\GroupDummy');
}
public function testInterface()
{
$this->assertInstanceOf('Symfony\Component\Serializer\Mapping\Loader\LoaderInterface', $this->loader);
}
public function testLoadClassMetadataReturnsTrueIfSuccessful()
{
$this->assertTrue($this->loader->loadClassMetadata($this->metadata));
}
public function testLoadClassMetadata()
{
$this->loader->loadClassMetadata($this->metadata);
$this->assertEquals(TestClassMetadataFactory::createXmlCLassMetadata(), $this->metadata);
}
public function testMaxDepth()
{
$classMetadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\MaxDepthDummy');
$this->loader->loadClassMetadata($classMetadata);
$attributesMetadata = $classMetadata->getAttributesMetadata();
$this->assertEquals(2, $attributesMetadata['foo']->getMaxDepth());
$this->assertEquals(3, $attributesMetadata['bar']->getMaxDepth());
}
}
@@ -1,78 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Mapping\Loader;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Mapping\ClassMetadata;
use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader;
use Symfony\Component\Serializer\Tests\Mapping\TestClassMetadataFactory;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class YamlFileLoaderTest extends TestCase
{
/**
* @var YamlFileLoader
*/
private $loader;
/**
* @var ClassMetadata
*/
private $metadata;
protected function setUp()
{
$this->loader = new YamlFileLoader(__DIR__.'/../../Fixtures/serialization.yml');
$this->metadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\GroupDummy');
}
public function testInterface()
{
$this->assertInstanceOf('Symfony\Component\Serializer\Mapping\Loader\LoaderInterface', $this->loader);
}
public function testLoadClassMetadataReturnsTrueIfSuccessful()
{
$this->assertTrue($this->loader->loadClassMetadata($this->metadata));
}
public function testLoadClassMetadataReturnsFalseWhenEmpty()
{
$loader = new YamlFileLoader(__DIR__.'/../../Fixtures/empty-mapping.yml');
$this->assertFalse($loader->loadClassMetadata($this->metadata));
}
public function testLoadClassMetadataReturnsThrowsInvalidMapping()
{
$this->expectException('Symfony\Component\Serializer\Exception\MappingException');
$loader = new YamlFileLoader(__DIR__.'/../../Fixtures/invalid-mapping.yml');
$loader->loadClassMetadata($this->metadata);
}
public function testLoadClassMetadata()
{
$this->loader->loadClassMetadata($this->metadata);
$this->assertEquals(TestClassMetadataFactory::createXmlCLassMetadata(), $this->metadata);
}
public function testMaxDepth()
{
$classMetadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\MaxDepthDummy');
$this->loader->loadClassMetadata($classMetadata);
$attributesMetadata = $classMetadata->getAttributesMetadata();
$this->assertEquals(2, $attributesMetadata['foo']->getMaxDepth());
$this->assertEquals(3, $attributesMetadata['bar']->getMaxDepth());
}
}
@@ -1,82 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Mapping;
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
use Symfony\Component\Serializer\Mapping\ClassMetadata;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class TestClassMetadataFactory
{
public static function createClassMetadata($withParent = false, $withInterface = false)
{
$expected = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\GroupDummy');
$foo = new AttributeMetadata('foo');
$foo->addGroup('a');
$expected->addAttributeMetadata($foo);
$bar = new AttributeMetadata('bar');
$bar->addGroup('b');
$bar->addGroup('c');
$bar->addGroup('name_converter');
$expected->addAttributeMetadata($bar);
$fooBar = new AttributeMetadata('fooBar');
$fooBar->addGroup('a');
$fooBar->addGroup('b');
$fooBar->addGroup('name_converter');
$expected->addAttributeMetadata($fooBar);
$symfony = new AttributeMetadata('symfony');
$expected->addAttributeMetadata($symfony);
if ($withParent) {
$kevin = new AttributeMetadata('kevin');
$kevin->addGroup('a');
$expected->addAttributeMetadata($kevin);
$coopTilleuls = new AttributeMetadata('coopTilleuls');
$coopTilleuls->addGroup('a');
$coopTilleuls->addGroup('b');
$expected->addAttributeMetadata($coopTilleuls);
}
if ($withInterface) {
$symfony->addGroup('a');
$symfony->addGroup('name_converter');
}
// load reflection class so that the comparison passes
$expected->getReflectionClass();
return $expected;
}
public static function createXmlCLassMetadata()
{
$expected = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\GroupDummy');
$foo = new AttributeMetadata('foo');
$foo->addGroup('group1');
$foo->addGroup('group2');
$expected->addAttributeMetadata($foo);
$bar = new AttributeMetadata('bar');
$bar->addGroup('group2');
$expected->addAttributeMetadata($bar);
return $expected;
}
}

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