diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md index fc441ba95acd..a82a7c6b2d9e 100644 --- a/src/Symfony/Component/Serializer/CHANGELOG.md +++ b/src/Symfony/Component/Serializer/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 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 @@ -24,6 +25,15 @@ CHANGELOG 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 ----- diff --git a/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php b/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php index ba1a62526c42..c453120fc6a0 100644 --- a/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php @@ -30,20 +30,34 @@ class CsvEncoder implements EncoderInterface, DecoderInterface const ESCAPE_FORMULAS_KEY = 'csv_escape_formulas'; const AS_COLLECTION_KEY = 'as_collection'; - private $delimiter; - private $enclosure; - private $escapeChar; - private $keySeparator; - private $escapeFormulas; private $formulasStartCharacters = array('=', '-', '+', '@'); + private $defaultContext = array( + self::DELIMITER_KEY => ',', + self::ENCLOSURE_KEY => '"', + self::ESCAPE_CHAR_KEY => '\\', + self::ESCAPE_FORMULAS_KEY => false, + self::HEADERS_KEY => array(), + self::KEY_SEPARATOR_KEY => '.', + ); - public function __construct(string $delimiter = ',', string $enclosure = '"', string $escapeChar = '\\', string $keySeparator = '.', bool $escapeFormulas = false) + /** + * @param array $defaultContext + */ + public function __construct($defaultContext = array(), string $enclosure = '"', string $escapeChar = '\\', string $keySeparator = '.', bool $escapeFormulas = false) { - $this->delimiter = $delimiter; - $this->enclosure = $enclosure; - $this->escapeChar = $escapeChar; - $this->keySeparator = $keySeparator; - $this->escapeFormulas = $escapeFormulas; + 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 = array( + 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->defaultContext = array_merge($this->defaultContext, $defaultContext); } /** @@ -200,14 +214,14 @@ private function flatten(array $array, array &$result, string $keySeparator, str } } - 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] : array(); - $escapeFormulas = isset($context[self::ESCAPE_FORMULAS_KEY]) ? $context[self::ESCAPE_FORMULAS_KEY] : $this->escapeFormulas; + $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]; 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))); diff --git a/src/Symfony/Component/Serializer/Encoder/JsonDecode.php b/src/Symfony/Component/Serializer/Encoder/JsonDecode.php index 5b0a432f3920..2722974d586a 100644 --- a/src/Symfony/Component/Serializer/Encoder/JsonDecode.php +++ b/src/Symfony/Component/Serializer/Encoder/JsonDecode.php @@ -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. + */ + const ASSOCIATIVE = 'json_decode_associative'; + + const OPTIONS = 'json_decode_options'; + + /** + * Specifies the recursion depth. + */ + const RECURSION_DEPTH = 'json_decode_recursion_depth'; + + private $defaultContext = array( + 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(bool $associative = false, int $depth = 512) + public function __construct($defaultContext = array(), int $depth = 512) { - $this->associative = $associative; - $this->recursionDepth = $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 = array( + self::ASSOCIATIVE => (bool) $defaultContext, + self::RECURSION_DEPTH => $depth, + ); + } + + $this->defaultContext = array_merge($this->defaultContext, $defaultContext); } /** @@ -47,7 +69,7 @@ public function __construct(bool $associative = false, int $depth = 512) * 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 @@ public function __construct(bool $associative = false, int $depth = 512) * 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. + * Specifies additional options as per documentation for json_decode * * @return mixed * @@ -66,11 +88,9 @@ public function __construct(bool $associative = false, int $depth = 512) */ public function decode($data, $format, array $context = array()) { - $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]; $decodedData = json_decode($data, $associative, $recursionDepth, $options); @@ -88,20 +108,4 @@ public function supportsDecoding($format) { return JsonEncoder::FORMAT === $format; } - - /** - * Merges the default options of the Json Decoder with the passed context. - * - * @return array - */ - private function resolveContext(array $context) - { - $defaultOptions = array( - 'json_decode_associative' => $this->associative, - 'json_decode_recursion_depth' => $this->recursionDepth, - 'json_decode_options' => 0, - ); - - return array_merge($defaultOptions, $context); - } } diff --git a/src/Symfony/Component/Serializer/Encoder/JsonEncode.php b/src/Symfony/Component/Serializer/Encoder/JsonEncode.php index 5cc30b75026f..63669d4ca696 100644 --- a/src/Symfony/Component/Serializer/Encoder/JsonEncode.php +++ b/src/Symfony/Component/Serializer/Encoder/JsonEncode.php @@ -20,11 +20,24 @@ */ class JsonEncode implements EncoderInterface { - private $options; + const OPTIONS = 'json_encode_options'; - public function __construct(int $bitmask = 0) + private $defaultContext = array( + self::OPTIONS => 0, + ); + + /** + * @param array $defaultContext + */ + public function __construct($defaultContext = array()) { - $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,11 +47,10 @@ public function __construct(int $bitmask = 0) */ public function encode($data, $format, array $context = array()) { - $context = $this->resolveContext($context); - - $encodedJson = json_encode($data, $context['json_encode_options']); + $jsonEncodeOptions = $context[self::OPTIONS] ?? $this->defaultContext[self::OPTIONS]; + $encodedJson = json_encode($data, $jsonEncodeOptions); - if (JSON_ERROR_NONE !== json_last_error() && (false === $encodedJson || !($context['json_encode_options'] & JSON_PARTIAL_OUTPUT_ON_ERROR))) { + if (JSON_ERROR_NONE !== json_last_error() && (false === $encodedJson || !($jsonEncodeOptions & JSON_PARTIAL_OUTPUT_ON_ERROR))) { throw new NotEncodableValueException(json_last_error_msg()); } @@ -52,14 +64,4 @@ public function supportsEncoding($format) { return JsonEncoder::FORMAT === $format; } - - /** - * Merge default json encode options with context. - * - * @return array - */ - private function resolveContext(array $context = array()) - { - return array_merge(array('json_encode_options' => $this->options), $context); - } } diff --git a/src/Symfony/Component/Serializer/Encoder/JsonEncoder.php b/src/Symfony/Component/Serializer/Encoder/JsonEncoder.php index f4950cb3b3f4..4690c9863c71 100644 --- a/src/Symfony/Component/Serializer/Encoder/JsonEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/JsonEncoder.php @@ -26,7 +26,7 @@ class JsonEncoder implements EncoderInterface, DecoderInterface public function __construct(JsonEncode $encodingImpl = null, JsonDecode $decodingImpl = null) { $this->encodingImpl = $encodingImpl ?: new JsonEncode(); - $this->decodingImpl = $decodingImpl ?: new JsonDecode(true); + $this->decodingImpl = $decodingImpl ?: new JsonDecode(array(JsonDecode::ASSOCIATIVE => true)); } /** diff --git a/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php index c04b7d845b52..d92266cfa83d 100644 --- a/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php @@ -30,30 +30,64 @@ class XmlEncoder implements EncoderInterface, DecoderInterface, NormalizationAwa const FORMAT = 'xml'; + const AS_COLLECTION = 'as_collection'; + + /** + * An array of ignored XML node types while decoding, each one of the DOM Predefined XML_* constants. + */ + const DECODER_IGNORED_NODE_TYPES = 'decoder_ignored_node_types'; + + /** + * An array of ignored XML node types while encoding, each one of the DOM Predefined XML_* constants. + */ + const ENCODER_IGNORED_NODE_TYPES = 'encoder_ignored_node_types'; + const ENCODING = 'xml_encoding'; + const FORMAT_OUTPUT = 'xml_format_output'; + + /** + * A bit field of LIBXML_* constants. + */ + const LOAD_OPTIONS = 'load_options'; + const REMOVE_EMPTY_TAGS = 'remove_empty_tags'; + const ROOT_NODE_NAME = 'xml_root_node_name'; + const STANDALONE = 'xml_standalone'; + const TYPE_CASE_ATTRIBUTES = 'xml_type_cast_attributes'; + const VERSION = 'xml_version'; + + private $defaultContext = array( + self::AS_COLLECTION => false, + self::DECODER_IGNORED_NODE_TYPES => array(XML_PI_NODE, XML_COMMENT_NODE), + self::ENCODER_IGNORED_NODE_TYPES => array(), + self::LOAD_OPTIONS => LIBXML_NONET | LIBXML_NOBLANKS, + self::REMOVE_EMPTY_TAGS => false, + self::ROOT_NODE_NAME => 'response', + self::TYPE_CASE_ATTRIBUTES => true, + ); + /** * @var \DOMDocument */ private $dom; private $format; private $context; - private $rootNodeName = 'response'; - private $loadOptions; - private $decoderIgnoredNodeTypes; - private $encoderIgnoredNodeTypes; /** - * Construct new XmlEncoder and allow to change the root node element name. - * - * @param int|null $loadOptions A bit field of LIBXML_* constants - * @param int[] $decoderIgnoredNodeTypes an array of ignored XML node types while decoding, each one of the DOM Predefined XML_* Constants - * @param int[] $encoderIgnoredNodeTypes an array of ignored XML node types while encoding, each one of the DOM Predefined XML_* Constants + * @param array $defaultContext */ - public function __construct(string $rootNodeName = 'response', int $loadOptions = null, array $decoderIgnoredNodeTypes = array(XML_PI_NODE, XML_COMMENT_NODE), array $encoderIgnoredNodeTypes = array()) + public function __construct($defaultContext = array(), int $loadOptions = null, array $decoderIgnoredNodeTypes = array(XML_PI_NODE, XML_COMMENT_NODE), array $encoderIgnoredNodeTypes = array()) { - $this->rootNodeName = $rootNodeName; - $this->loadOptions = null !== $loadOptions ? $loadOptions : LIBXML_NONET | LIBXML_NOBLANKS; - $this->decoderIgnoredNodeTypes = $decoderIgnoredNodeTypes; - $this->encoderIgnoredNodeTypes = $encoderIgnoredNodeTypes; + 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 = array( + 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); } /** @@ -61,11 +95,13 @@ public function __construct(string $rootNodeName = 'response', int $loadOptions */ public function encode($data, $format, array $context = array()) { + $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(\in_array(XML_PI_NODE, $this->encoderIgnoredNodeTypes, true) ? $data->documentElement : null); + 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; @@ -79,7 +115,7 @@ public function encode($data, $format, array $context = array()) $this->appendNode($this->dom, $data, $xmlRootNodeName); } - return $this->dom->saveXML(\in_array(XML_PI_NODE, $this->encoderIgnoredNodeTypes, true) ? $this->dom->documentElement : null); + return $this->dom->saveXML($ignorePiNode ? $this->dom->documentElement : null); } /** @@ -96,7 +132,7 @@ public function decode($data, $format, array $context = array()) 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); libxml_disable_entity_loader($disableEntities); @@ -108,11 +144,12 @@ public function decode($data, $format, array $context = array()) } $rootNode = null; + $decoderIgnoredNodeTypes = $context[self::DECODER_IGNORED_NODE_TYPES] ?? $this->defaultContext[self::DECODER_IGNORED_NODE_TYPES]; foreach ($dom->childNodes as $child) { if (XML_DOCUMENT_TYPE_NODE === $child->nodeType) { throw new NotEncodableValueException('Document types are not allowed.'); } - if (!$rootNode && !\in_array($child->nodeType, $this->decoderIgnoredNodeTypes, true)) { + if (!$rootNode && !\in_array($child->nodeType, $decoderIgnoredNodeTypes, true)) { $rootNode = $child; } } @@ -169,21 +206,29 @@ public function supportsDecoding($format) /** * 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]; } final protected function appendXMLString(\DOMNode $node, string $val): bool @@ -291,7 +336,7 @@ private function parseXmlAttributes(\DOMNode $node, array $context = array()): a } $data = array(); - $typeCastAttributes = $this->resolveXmlTypeCastAttributes($context); + $typeCastAttributes = (bool) ($context[self::TYPE_CASE_ATTRIBUTES] ?? $this->defaultContext[self::TYPE_CASE_ATTRIBUTES]); foreach ($node->attributes as $attr) { if (!is_numeric($attr->nodeValue) || !$typeCastAttributes) { @@ -328,9 +373,9 @@ private function parseXmlValue(\DOMNode $node, array $context = array()) } $value = array(); - + $decoderIgnoredNodeTypes = $context[self::DECODER_IGNORED_NODE_TYPES] ?? $this->defaultContext[self::DECODER_IGNORED_NODE_TYPES]; foreach ($node->childNodes as $subnode) { - if (\in_array($subnode->nodeType, $this->decoderIgnoredNodeTypes, true)) { + if (\in_array($subnode->nodeType, $decoderIgnoredNodeTypes, true)) { continue; } @@ -347,8 +392,9 @@ private function parseXmlValue(\DOMNode $node, array $context = array()) } } + $asCollection = $context[self::AS_COLLECTION] ?? $this->defaultContext[self::AS_COLLECTION]; foreach ($value as $key => $val) { - if (empty($context['as_collection']) && \is_array($val) && 1 === \count($val)) { + if (!$asCollection && \is_array($val) && 1 === \count($val)) { $value[$key] = current($val); } } @@ -366,6 +412,8 @@ private function parseXmlValue(\DOMNode $node, array $context = array()) private function buildXml(\DOMNode $parentNode, $data, string $xmlRootNodeName = null): bool { $append = true; + $removeEmptyTags = $this->context[self::REMOVE_EMPTY_TAGS] ?? $this->defaultContext[self::REMOVE_EMPTY_TAGS] ?? false; + $encoderIgnoredNodeTypes = $this->context[self::ENCODER_IGNORED_NODE_TYPES] ?? $this->defaultContext[self::ENCODER_IGNORED_NODE_TYPES]; if (\is_array($data) || ($data instanceof \Traversable && !$this->serializer->supportsNormalization($data, $this->format))) { foreach ($data as $key => $data) { @@ -378,7 +426,7 @@ private function buildXml(\DOMNode $parentNode, $data, string $xmlRootNodeName = } elseif ('#' === $key) { $append = $this->selectNodeType($parentNode, $data); } elseif ('#comment' === $key) { - if (!\in_array(XML_COMMENT_NODE, $this->encoderIgnoredNodeTypes, true)) { + if (!\in_array(XML_COMMENT_NODE, $encoderIgnoredNodeTypes, true)) { $append = $this->appendComment($parentNode, $data); } } elseif (\is_array($data) && false === is_numeric($key)) { @@ -397,7 +445,7 @@ private function buildXml(\DOMNode $parentNode, $data, string $xmlRootNodeName = } } 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']) { + } elseif (null !== $data || !$removeEmptyTags) { $append = $this->appendNode($parentNode, $data, $key); } } @@ -487,26 +535,6 @@ private function selectNodeType(\DOMNode $node, $val): bool return true; } - /** - * Get real XML root node name, taking serializer options into account. - */ - private function resolveXmlRootName(array $context = array()): string - { - return isset($context['xml_root_node_name']) - ? $context['xml_root_node_name'] - : $this->rootNodeName; - } - - /** - * Get XML option for type casting attributes Defaults to true. - */ - private function resolveXmlTypeCastAttributes(array $context = array()): bool - { - return isset($context['xml_type_cast_attributes']) - ? (bool) $context['xml_type_cast_attributes'] - : true; - } - /** * Create a DOM document, taking serializer options into account. */ @@ -517,17 +545,17 @@ private function createDomDocument(array $context): \DOMDocument // Set an attribute on the DOM document specifying, as part of the XML declaration, $xmlOptions = array( // 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; } } diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index 78102cb6a961..3eb9e57002f2 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -38,14 +38,30 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn const ATTRIBUTES = 'attributes'; const ALLOW_EXTRA_ATTRIBUTES = 'allow_extra_attributes'; const DEFAULT_CONSTRUCTOR_ARGUMENTS = 'default_constructor_arguments'; + const CALLBACKS = 'callbacks'; + const CIRCULAR_REFERENCE_HANDLER = 'circular_reference_handler'; + const IGNORED_ATTRIBUTES = 'ignored_attributes'; /** - * @var int + * @internal + */ + const CIRCULAR_REFERENCE_LIMIT_COUNTERS = 'circular_reference_limit_counters'; + + protected $defaultContext = array( + self::ALLOW_EXTRA_ATTRIBUTES => true, + self::CIRCULAR_REFERENCE_LIMIT => 1, + self::IGNORED_ATTRIBUTES => array(), + ); + + /** + * @deprecated since Symfony 4.2 */ protected $circularReferenceLimit = 1; /** - * @var callable + * @deprecated since Symfony 4.2 + * + * @var callable|null */ protected $circularReferenceHandler; @@ -60,31 +76,42 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn protected $nameConverter; /** - * @var array + * @deprecated since Symfony 4.2 */ protected $callbacks = array(); /** - * @var array + * @deprecated since Symfony 4.2 */ protected $ignoredAttributes = array(); /** - * @var array + * @deprecated since Symfony 4.2 */ protected $camelizedAttributes = array(); /** * 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 = array()) { $this->classMetadataFactory = $classMetadataFactory; $this->nameConverter = $nameConverter; + $this->defaultContext = array_merge($this->defaultContext, $defaultContext); + + if (\is_array($this->defaultContext[self::CALLBACKS] ?? null)) { + foreach ($this->defaultContext[self::CALLBACKS] as $attribute => $callback) { + if (!\is_callable($callback)) { + throw new InvalidArgumentException(sprintf('The given callback for attribute "%s" is not callable.', $attribute)); + } + } + } } /** - * Set circular reference limit. + * Sets circular reference limit. + * + * @deprecated since Symfony 4.2 * * @param int $circularReferenceLimit Limit of iterations for the same object * @@ -92,13 +119,17 @@ public function __construct(ClassMetadataFactoryInterface $classMetadataFactory */ 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 * * @param callable $circularReferenceHandler * @@ -106,13 +137,17 @@ public function setCircularReferenceLimit($circularReferenceLimit) */ 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 normalization callbacks. + * + * @deprecated since Symfony 4.2 * * @param callable[] $callbacks Help normalize the result * @@ -122,24 +157,30 @@ public function setCircularReferenceHandler(callable $circularReferenceHandler) */ 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; } @@ -166,16 +207,17 @@ protected function isCircularReference($object, &$context) { $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; @@ -205,8 +247,9 @@ protected function handleCircularReference($object/*, string $format = null, arr $format = \func_num_args() > 1 ? func_get_arg(1) : null; $context = \func_num_args() > 2 ? func_get_arg(2) : array(); - if ($this->circularReferenceHandler) { - return \call_user_func($this->circularReferenceHandler, $object, $format, $context); + $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)); @@ -225,18 +268,18 @@ protected function handleCircularReference($object/*, string $format = null, arr */ 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]) || is_scalar($context[static::GROUPS]))) { - $groups = (array) $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; } @@ -267,17 +310,19 @@ protected function getAllowedAttributes($classOrObject, array $context, $attribu */ protected function isAllowedAttribute($classOrObject, $attribute, $format = null, array $context = array()) { - 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; @@ -335,8 +380,8 @@ protected function getConstructor(array &$data, $class, array &$context, \Reflec */ protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes, string $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; } @@ -371,7 +416,7 @@ protected function instantiateObject(array &$data, $class, array &$context, \Ref try { if (null !== $constructorParameter->getClass()) { 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', $constructorParameter->getClass(), static::class)); + throw new LogicException(sprintf('Cannot create an instance of %s from serialized data because the serializer inject in "%s" is not a denormalizer', $constructorParameter->getClass(), self::class)); } $parameterClass = $constructorParameter->getClass()->getName(); $parameterData = $this->serializer->denormalize($parameterData, $parameterClass, $format, $this->createChildContext($context, $paramName)); @@ -388,8 +433,8 @@ protected function instantiateObject(array &$data, $class, array &$context, \Ref // Don't run set for a parameter passed to the constructor $params[] = $parameterData; unset($data[$key]); - } elseif (isset($context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key])) { - $params[] = $context[static::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key]; + } elseif (null !== $param = $context[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key] ?? $this->defaultContext[self::DEFAULT_CONSTRUCTOR_ARGUMENTS][$class][$key] ?? null) { + $params[] = $param; } elseif ($constructorParameter->isDefaultValueAvailable()) { $params[] = $constructorParameter->getDefaultValue(); } else { diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index 6864f8e145d1..649df9c3a838 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -36,12 +36,16 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer const DEPTH_KEY_PATTERN = 'depth_%s::%s'; const DISABLE_TYPE_ENFORCEMENT = 'disable_type_enforcement'; const SKIP_NULL_VALUES = 'skip_null_values'; + const MAX_DEPTH_HANDLER = 'max_depth_handler'; + const EXCLUDE_FROM_CACHE_KEY = 'exclude_from_cache_key'; private $propertyTypeExtractor; private $typesCache = array(); private $attributesCache = array(); /** + * @deprecated since Symfony 4.2 + * * @var callable|null */ private $maxDepthHandler; @@ -52,9 +56,10 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer */ protected $classDiscriminatorResolver; - public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null) + public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = array()) { - parent::__construct($classMetadataFactory, $nameConverter); + parent::__construct($classMetadataFactory, $nameConverter, $defaultContext); + $this->defaultContext[self::EXCLUDE_FROM_CACHE_KEY] = array(self::CIRCULAR_REFERENCE_LIMIT_COUNTERS); $this->propertyTypeExtractor = $propertyTypeExtractor; @@ -91,20 +96,25 @@ public function normalize($object, $format = null, array $context = array()) $attributes = $this->getAttributes($object, $format, $context); $class = $this->objectClassResolver ? \call_user_func($this->objectClassResolver, $object) : \get_class($object); $attributesMetadata = $this->classMetadataFactory ? $this->classMetadataFactory->getMetadataFor($class)->getAttributesMetadata() : null; + $maxDepthHandler = $context[self::MAX_DEPTH_HANDLER] ?? $this->defaultContext[self::MAX_DEPTH_HANDLER] ?? $this->maxDepthHandler; foreach ($attributes as $attribute) { $maxDepthReached = false; - if (null !== $attributesMetadata && ($maxDepthReached = $this->isMaxDepthReached($attributesMetadata, $class, $attribute, $context)) && !$this->maxDepthHandler) { + if (null !== $attributesMetadata && ($maxDepthReached = $this->isMaxDepthReached($attributesMetadata, $class, $attribute, $context)) && !$maxDepthHandler) { continue; } $attributeValue = $this->getAttributeValue($object, $attribute, $format, $context); if ($maxDepthReached) { - $attributeValue = \call_user_func($this->maxDepthHandler, $attributeValue, $object, $attribute, $format, $context); + $attributeValue = \call_user_func($maxDepthHandler, $attributeValue, $object, $attribute, $format, $context); } - if (isset($this->callbacks[$attribute])) { - $attributeValue = \call_user_func($this->callbacks[$attribute], $attributeValue, $object, $attribute, $format, $context); + /** + * @var $callback callable|null + */ + $callback = $context[self::CALLBACKS][$attribute] ?? $this->defaultContext[self::CALLBACKS][$attribute] ?? $this->callbacks[$attribute] ?? null; + if ($callback) { + $attributeValue = $callback($attributeValue, $object, $attribute, $format, $context); } if (null !== $attributeValue && !is_scalar($attributeValue)) { @@ -175,7 +185,7 @@ protected function getAttributes($object, $format = null, array $context) return $allowedAttributes; } - if (isset($context['attributes'])) { + if ($context[self::ATTRIBUTES] ?? $this->defaultContext[self::ATTRIBUTES] ?? false) { return $this->extractAttributes($object, $format, $context); } @@ -217,9 +227,13 @@ abstract protected function getAttributeValue($object, $attribute, $format = nul /** * 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; } @@ -253,7 +267,7 @@ public function denormalize($data, $class, $format = null, array $context = arra } if ((false !== $allowedAttributes && !\in_array($attribute, $allowedAttributes)) || !$this->isAllowedAttribute($class, $attribute, $format, $context)) { - if (isset($context[self::ALLOW_EXTRA_ATTRIBUTES]) && !$context[self::ALLOW_EXTRA_ATTRIBUTES]) { + if (!($context[self::ALLOW_EXTRA_ATTRIBUTES] ?? $this->defaultContext[self::ALLOW_EXTRA_ATTRIBUTES])) { $extraAttributes[] = $attribute; } @@ -354,7 +368,7 @@ private function validateAndDenormalize(string $currentClass, string $attribute, } } - if (!empty($context[self::DISABLE_TYPE_ENFORCEMENT])) { + if ($context[self::DISABLE_TYPE_ENFORCEMENT] ?? $this->defaultContext[self::DISABLE_TYPE_ENFORCEMENT] ?? false) { return $data; } @@ -405,7 +419,7 @@ private function getTypes(string $currentClass, string $attribute) */ private function updateData(array $data, string $attribute, $attributeValue, string $class, ?string $format, array $context): array { - if (null === $attributeValue && ($context[self::SKIP_NULL_VALUES] ?? false)) { + if (null === $attributeValue && ($context[self::SKIP_NULL_VALUES] ?? $this->defaultContext[self::SKIP_NULL_VALUES] ?? false)) { return $data; } @@ -425,16 +439,16 @@ private function updateData(array $data, string $attribute, $attributeValue, str */ 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; @@ -457,6 +471,11 @@ private function isMaxDepthReached(array $attributesMetadata, string $class, str */ 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]); + try { return md5($format.serialize($context)); } catch (\Exception $exception) { diff --git a/src/Symfony/Component/Serializer/Normalizer/ConstraintViolationListNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ConstraintViolationListNormalizer.php index bc9aa60bd3a0..dc24f6ec16bd 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ConstraintViolationListNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ConstraintViolationListNormalizer.php @@ -24,6 +24,18 @@ */ class ConstraintViolationListNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface { + const INSTANCE = 'instance'; + const STATUS = 'status'; + const TITLE = 'title'; + const TYPE = 'type'; + + private $defaultContext; + + public function __construct($defaultContext = array()) + { + $this->defaultContext = $defaultContext; + } + /** * {@inheritdoc} */ @@ -49,17 +61,17 @@ public function normalize($object, $format = null, array $context = array()) } $result = array( - 'type' => $context['type'] ?? 'https://symfony.com/errors/validation', - 'title' => $context['title'] ?? 'Validation Failed', + 'type' => $context[self::TYPE] ?? $this->defaultContext[self::TYPE] ?? 'https://symfony.com/errors/validation', + 'title' => $context[self::TITLE] ?? $this->defaultContext[self::TITLE] ?? 'Validation Failed', ); - if (isset($context['status'])) { - $result['status'] = $context['status']; + if (null !== $status = ($context[self::STATUS] ?? $this->defaultContext[self::STATUS] ?? null)) { + $result['status'] = $status; } if ($messages) { $result['detail'] = implode("\n", $messages); } - if (isset($context['instance'])) { - $result['instance'] = $context['instance']; + if (null !== $instance = ($context[self::INSTANCE] ?? $this->defaultContext[self::INSTANCE] ?? null)) { + $result['instance'] = $instance; } return $result + array('violations' => $violations); diff --git a/src/Symfony/Component/Serializer/Normalizer/DateIntervalNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/DateIntervalNormalizer.php index 2dce4d5b5416..9a050ae003ed 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DateIntervalNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/DateIntervalNormalizer.php @@ -24,11 +24,22 @@ class DateIntervalNormalizer implements NormalizerInterface, DenormalizerInterfa { const FORMAT_KEY = 'dateinterval_format'; - private $format; + private $defaultContext = array( + self::FORMAT_KEY => 'P%yY%mM%dDT%hH%iM%sS', + ); - public function __construct(string $format = 'P%yY%mM%dDT%hH%iM%sS') + /** + * @param array $defaultContext + */ + public function __construct($defaultContext = array()) { - $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 = array(self::FORMAT_KEY => (string) $defaultContext); + } + + $this->defaultContext = array_merge($this->defaultContext, $defaultContext); } /** @@ -42,9 +53,7 @@ public function normalize($object, $format = null, array $context = array()) 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]); } /** @@ -79,7 +88,7 @@ public function denormalize($data, $class, $format = null, array $context = arra 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]; $valuePattern = '/^'.preg_replace('/%([yYmMdDhHiIsSwW])(\w)/', '(?P<$1>\d+)$2', $dateIntervalFormat).'$/'; if (!preg_match($valuePattern, $data)) { diff --git a/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php index b2ebe97bd57e..26c48e5fd49f 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php @@ -25,8 +25,7 @@ class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface, const FORMAT_KEY = 'datetime_format'; const TIMEZONE_KEY = 'datetime_timezone'; - private $format; - private $timezone; + private $defaultContext; private static $supportedTypes = array( \DateTimeInterface::class => true, @@ -34,10 +33,24 @@ class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface, \DateTime::class => true, ); - public function __construct(?string $format = \DateTime::RFC3339, \DateTimeZone $timezone = null) + /** + * @param array $defaultContext + */ + public function __construct($defaultContext = array(), \DateTimeZone $timezone = null) { - $this->format = $format; - $this->timezone = $timezone; + $this->defaultContext = array( + 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 = array(self::FORMAT_KEY => (string) $defaultContext); + $defaultContext[self::TIMEZONE_KEY] = $timezone; + } + + $this->defaultContext = array_merge($this->defaultContext, $defaultContext); } /** @@ -51,14 +64,14 @@ public function normalize($object, $format = null, array $context = array()) 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) { $object = (new \DateTimeImmutable('@'.$object->getTimestamp()))->setTimezone($timezone); } - return $object->format($format); + return $object->format($dateTimeFormat); } /** @@ -76,7 +89,7 @@ public function supportsNormalization($data, $format = null) */ public function denormalize($data, $class, $format = null, array $context = array()) { - $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) { @@ -142,8 +155,7 @@ private function formatDateTimeErrors(array $errors) private function getTimezone(array $context) { - $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; } diff --git a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php index a6ea398e6265..f0f749ad74fc 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php @@ -30,13 +30,13 @@ class ObjectNormalizer extends AbstractObjectNormalizer { protected $propertyAccessor; - public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null) + public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyAccessorInterface $propertyAccessor = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = array()) { if (!\class_exists(PropertyAccess::class)) { throw new LogicException('The ObjectNormalizer class requires the "PropertyAccess" component. Install "symfony/property-access" to use it.'); } - parent::__construct($classMetadataFactory, $nameConverter, $propertyTypeExtractor, $classDiscriminatorResolver, $objectClassResolver); + parent::__construct($classMetadataFactory, $nameConverter, $propertyTypeExtractor, $classDiscriminatorResolver, $objectClassResolver, $defaultContext); $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); } diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php index 9a7103449614..65ced222a976 100644 --- a/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php @@ -100,7 +100,26 @@ public function testEncodeNestedArrays() public function testEncodeCustomSettings() { - $this->encoder = new CsvEncoder(';', "'", '|', '-'); + $this->doTestEncodeCustomSettings(); + } + + public function testLegacyEncodeCustomSettings() + { + $this->doTestEncodeCustomSettings(true); + } + + private function doTestEncodeCustomSettings(bool $legacy = false) + { + if ($legacy) { + $this->encoder = new CsvEncoder(';', "'", '|', '-'); + } else { + $this->encoder = new CsvEncoder(array( + CsvEncoder::DELIMITER_KEY => ';', + CsvEncoder::ENCLOSURE_KEY => "'", + CsvEncoder::ESCAPE_CHAR_KEY => '|', + CsvEncoder::KEY_SEPARATOR_KEY => '-', + )); + } $value = array('a' => 'he\'llo', 'c' => array('d' => 'foo')); @@ -175,7 +194,21 @@ public function testEncodeCustomHeaders() public function testEncodeFormulas() { - $this->encoder = new CsvEncoder(',', '"', '\\', '.', true); + $this->doTestEncodeFormulas(); + } + + public function testLegacyEncodeFormulas() + { + $this->doTestEncodeFormulas(true); + } + + private function doTestEncodeFormulas(bool $legacy = false) + { + if ($legacy) { + $this->encoder = new CsvEncoder(',', '"', '\\', '.', true); + } else { + $this->encoder = new CsvEncoder(array(CsvEncoder::ESCAPE_FORMULAS_KEY => true)); + } $this->assertSame(<<<'CSV' 0 @@ -378,7 +411,26 @@ public function testDecodeNestedArrays() public function testDecodeCustomSettings() { - $this->encoder = new CsvEncoder(';', "'", '|', '-'); + $this->doTestDecodeCustomSettings(); + } + + public function testLegacyDecodeCustomSettings() + { + $this->doTestDecodeCustomSettings(true); + } + + private function doTestDecodeCustomSettings(bool $legacy = false) + { + if ($legacy) { + $this->encoder = new CsvEncoder(';', "'", '|', '-'); + } else { + $this->encoder = new CsvEncoder(array( + CsvEncoder::DELIMITER_KEY => ';', + CsvEncoder::ENCLOSURE_KEY => "'", + CsvEncoder::ESCAPE_CHAR_KEY => '|', + CsvEncoder::KEY_SEPARATOR_KEY => '-', + )); + } $expected = array(array('a' => 'hell\'o', 'bar' => array('baz' => 'b'))); $this->assertEquals($expected, $this->encoder->decode(<<<'CSV' diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php index 3fa60122e1b0..6ef68c13f926 100644 --- a/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php @@ -47,6 +47,9 @@ public function testEncodeScalar() $this->assertEquals($expected, $this->encoder->encode($obj, 'xml')); } + /** + * @group legacy + */ public function testSetRootNodeName() { $obj = new ScalarDummy(); @@ -543,6 +546,16 @@ public function testDecodeIgnoreComments() } public function testDecodePreserveComments() + { + $this->doTestDecodePreserveComments(); + } + + public function testLegacyDecodePreserveComments() + { + $this->doTestDecodePreserveComments(true); + } + + private function doTestDecodePreserveComments(bool $legacy = false) { $source = <<<'XML' @@ -559,7 +572,14 @@ public function testDecodePreserveComments() XML; - $this->encoder = new XmlEncoder('people', null, array(XML_PI_NODE)); + if ($legacy) { + $this->encoder = new XmlEncoder('people', null, array(XML_PI_NODE)); + } else { + $this->encoder = new XmlEncoder(array( + XmlEncoder::ROOT_NODE_NAME => 'people', + XmlEncoder::DECODER_IGNORED_NODE_TYPES => array(XML_PI_NODE), + )); + } $serializer = new Serializer(array(new CustomNormalizer()), array('xml' => new XmlEncoder())); $this->encoder->setSerializer($serializer); @@ -573,7 +593,21 @@ public function testDecodePreserveComments() public function testDecodeAlwaysAsCollection() { - $this->encoder = new XmlEncoder('response', null); + $this->doTestDecodeAlwaysAsCollection(); + } + + public function testLegacyDecodeAlwaysAsCollection() + { + $this->doTestDecodeAlwaysAsCollection(true); + } + + private function doTestDecodeAlwaysAsCollection(bool $legacy = false) + { + if ($legacy) { + $this->encoder = new XmlEncoder('response', null); + } else { + $this->encoder = new XmlEncoder(array(XmlEncoder::ROOT_NODE_NAME => 'response')); + } $serializer = new Serializer(array(new CustomNormalizer()), array('xml' => new XmlEncoder())); $this->encoder->setSerializer($serializer); @@ -773,9 +807,26 @@ public function testEncodeComment() $this->assertEquals($expected, $this->encoder->encode($data, 'xml')); } - public function testEncodeWithoutPI() + public function testEncodeWithoutPi() { - $encoder = new XmlEncoder('response', null, array(), array(XML_PI_NODE)); + $this->doTestEncodeWithoutPi(); + } + + public function testLegacyEncodeWithoutPi() + { + $this->doTestEncodeWithoutPi(true); + } + + private function doTestEncodeWithoutPi(bool $legacy = false) + { + if ($legacy) { + $encoder = new XmlEncoder('response', null, array(), array(XML_PI_NODE)); + } else { + $encoder = new XmlEncoder(array( + XmlEncoder::ROOT_NODE_NAME => 'response', + XmlEncoder::ENCODER_IGNORED_NODE_TYPES => array(XML_PI_NODE), + )); + } $expected = ''; @@ -784,7 +835,24 @@ public function testEncodeWithoutPI() public function testEncodeWithoutComment() { - $encoder = new XmlEncoder('response', null, array(), array(XML_COMMENT_NODE)); + $this->doTestEncodeWithoutComment(); + } + + public function testLegacyEncodeWithoutComment() + { + $this->doTestEncodeWithoutComment(true); + } + + private function doTestEncodeWithoutComment(bool $legacy = false) + { + if ($legacy) { + $encoder = new XmlEncoder('response', null, array(), array(XML_COMMENT_NODE)); + } else { + $encoder = new XmlEncoder(array( + XmlEncoder::ROOT_NODE_NAME => 'response', + XmlEncoder::ENCODER_IGNORED_NODE_TYPES => array(XML_COMMENT_NODE), + )); + } $expected = <<<'XML' diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/DateIntervalNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/DateIntervalNormalizerTest.php index f6dc6c2475e5..86478bb413e6 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/DateIntervalNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/DateIntervalNormalizerTest.php @@ -58,7 +58,21 @@ public function testNormalizeUsingFormatPassedInContext($format, $output, $input */ public function testNormalizeUsingFormatPassedInConstructor($format, $output, $input) { - $this->assertEquals($output, (new DateIntervalNormalizer($format))->normalize(new \DateInterval($input))); + $this->doTestNormalizeUsingFormatPassedInConstructor($format, $output, $input); + } + + /** + * @dataProvider dataProviderISO + */ + public function testLegacyNormalizeUsingFormatPassedInConstructor($format, $output, $input) + { + $this->doTestNormalizeUsingFormatPassedInConstructor($format, $output, $input, true); + } + + private function doTestNormalizeUsingFormatPassedInConstructor($format, $output, $input, bool $legacy = false) + { + $normalizer = $legacy ? new DateIntervalNormalizer($format) : new DateIntervalNormalizer(array(DateIntervalNormalizer::FORMAT_KEY => $format)); + $this->assertEquals($output, $normalizer->normalize(new \DateInterval($input))); } /** @@ -94,7 +108,21 @@ public function testDenormalizeUsingFormatPassedInContext($format, $input, $outp */ public function testDenormalizeUsingFormatPassedInConstructor($format, $input, $output) { - $this->assertDateIntervalEquals(new \DateInterval($output), (new DateIntervalNormalizer($format))->denormalize($input, \DateInterval::class)); + $this->doTestDenormalizeUsingFormatPassedInConstructor($format, $input, $output); + } + + /** + * @dataProvider dataProviderISO + */ + public function testLegacyDenormalizeUsingFormatPassedInConstructor($format, $input, $output) + { + $this->doTestDenormalizeUsingFormatPassedInConstructor($format, $input, $output, true); + } + + private function doTestDenormalizeUsingFormatPassedInConstructor($format, $input, $output, bool $legacy = false) + { + $normalizer = $legacy ? new DateIntervalNormalizer($format) : new DateIntervalNormalizer(array(DateIntervalNormalizer::FORMAT_KEY => $format)); + $this->assertDateIntervalEquals(new \DateInterval($output), $normalizer->denormalize($input, \DateInterval::class)); } /** diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php index 178519b30e68..87edb8df4c99 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php @@ -49,12 +49,37 @@ public function testNormalizeUsingFormatPassedInContext() public function testNormalizeUsingFormatPassedInConstructor() { - $this->assertEquals('16', (new DateTimeNormalizer('y'))->normalize(new \DateTime('2016/01/01', new \DateTimeZone('UTC')))); + $this->doTestNormalizeUsingFormatPassedInConstructor(); + } + + public function testLegacyNormalizeUsingFormatPassedInConstructor() + { + $this->doTestNormalizeUsingFormatPassedInConstructor(true); + } + + private function doTestNormalizeUsingFormatPassedInConstructor(bool $legacy = false) + { + $normalizer = $legacy ? new DateTimeNormalizer('y') : new DateTimeNormalizer(array(DateTimeNormalizer::FORMAT_KEY => 'y')); + $this->assertEquals('16', $normalizer->normalize(new \DateTime('2016/01/01', new \DateTimeZone('UTC')))); } public function testNormalizeUsingTimeZonePassedInConstructor() { - $normalizer = new DateTimeNormalizer(\DateTime::RFC3339, new \DateTimeZone('Japan')); + $this->doTestNormalizeUsingTimeZonePassedInConstructor(); + } + + public function testLegacyNormalizeUsingTimeZonePassedInConstructor() + { + $this->doTestNormalizeUsingTimeZonePassedInConstructor(true); + } + + private function doTestNormalizeUsingTimeZonePassedInConstructor(bool $legacy = false) + { + if ($legacy) { + $normalizer = new DateTimeNormalizer(\DateTime::RFC3339, new \DateTimeZone('Japan')); + } else { + $normalizer = new DateTimeNormalizer(array(DateTimeNormalizer::TIMEZONE_KEY => new \DateTimeZone('Japan'))); + } $this->assertSame('2016-12-01T00:00:00+09:00', $normalizer->normalize(new \DateTime('2016/12/01', new \DateTimeZone('Japan')))); $this->assertSame('2016-12-01T09:00:00+09:00', $normalizer->normalize(new \DateTime('2016/12/01', new \DateTimeZone('UTC')))); @@ -103,10 +128,20 @@ public function testDenormalize() } public function testDenormalizeUsingTimezonePassedInConstructor() + { + $this->doTestDenormalizeUsingTimezonePassedInConstructor(); + } + + public function testLegacyDenormalizeUsingTimezonePassedInConstructor() + { + $this->doTestDenormalizeUsingTimezonePassedInConstructor(true); + } + + private function doTestDenormalizeUsingTimezonePassedInConstructor(bool $legacy = false) { $timezone = new \DateTimeZone('Japan'); $expected = new \DateTime('2016/12/01 17:35:00', $timezone); - $normalizer = new DateTimeNormalizer(null, $timezone); + $normalizer = $legacy ? new DateTimeNormalizer(null, $timezone) : new DateTimeNormalizer(array(DateTimeNormalizer::TIMEZONE_KEY => $timezone)); $this->assertEquals($expected, $normalizer->denormalize('2016.12.01 17:35:00', \DateTime::class, null, array( DateTimeNormalizer::FORMAT_KEY => 'Y.m.d H:i:s', diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php index 8940f70a4f4f..07277579b3b5 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -37,9 +37,14 @@ class GetSetMethodNormalizerTest extends TestCase private $serializer; protected function setUp() + { + $this->createNormalizer(); + } + + private function createNormalizer(array $defaultContext = array()) { $this->serializer = $this->getMockBuilder(__NAMESPACE__.'\SerializerNormalizer')->getMock(); - $this->normalizer = new GetSetMethodNormalizer(); + $this->normalizer = new GetSetMethodNormalizer(null, null, null, null, null, $defaultContext); $this->normalizer->setSerializer($this->serializer); } @@ -285,10 +290,22 @@ public function testGroupsDenormalizeWithNameConverter() */ public function testCallbacks($callbacks, $value, $result, $message) { - $this->normalizer->setCallbacks($callbacks); + $this->doTestCallbacks($callbacks, $value, $result, $message); + } - $obj = new GetConstructorDummy('', $value, true); + /** + * @dataProvider provideCallbacks + */ + public function testLegacyCallbacks($callbacks, $value, $result, $message) + { + $this->doTestCallbacks($callbacks, $value, $result, $message, true); + } + private function doTestCallbacks($callbacks, $value, $result, $message, bool $legacy = false) + { + $legacy ? $this->normalizer->setCallbacks($callbacks) : $this->createNormalizer(array(GetSetMethodNormalizer::CALLBACKS => $callbacks)); + + $obj = new GetConstructorDummy('', $value, true); $this->assertEquals( $result, $this->normalizer->normalize($obj, 'any'), @@ -301,7 +318,21 @@ public function testCallbacks($callbacks, $value, $result, $message) */ public function testUncallableCallbacks() { - $this->normalizer->setCallbacks(array('bar' => null)); + $this->doTestUncallableCallbacks(); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testLegacyUncallableCallbacks() + { + $this->doTestUncallableCallbacks(true); + } + + private function doTestUncallableCallbacks(bool $legacy = false) + { + $callbacks = array('bar' => null); + $legacy ? $this->normalizer->setCallbacks($callbacks) : $this->createNormalizer(array(GetSetMethodNormalizer::CALLBACKS => $callbacks)); $obj = new GetConstructorDummy('baz', 'quux', true); @@ -310,7 +341,18 @@ public function testUncallableCallbacks() public function testIgnoredAttributes() { - $this->normalizer->setIgnoredAttributes(array('foo', 'bar', 'baz', 'camelCase', 'object')); + $this->doTestIgnoredAttributes(); + } + + public function testLegacyIgnoredAttributes() + { + $this->doTestIgnoredAttributes(true); + } + + private function doTestIgnoredAttributes(bool $legacy = false) + { + $ignoredAttributes = array('foo', 'bar', 'baz', 'camelCase', 'object'); + $legacy ? $this->normalizer->setIgnoredAttributes($ignoredAttributes) : $this->createNormalizer(array(GetSetMethodNormalizer::IGNORED_ATTRIBUTES => $ignoredAttributes)); $obj = new GetSetDummy(); $obj->setFoo('foo'); @@ -404,12 +446,24 @@ public function testUnableToNormalizeObjectAttribute() */ public function testUnableToNormalizeCircularReference() { - $serializer = new Serializer(array($this->normalizer)); - $this->normalizer->setSerializer($serializer); - $this->normalizer->setCircularReferenceLimit(2); + $this->doTestUnableToNormalizeCircularReference(); + } - $obj = new CircularReferenceDummy(); + /** + * @expectedException \Symfony\Component\Serializer\Exception\CircularReferenceException + */ + public function testLegacyUnableToNormalizeCircularReference() + { + $this->doTestUnableToNormalizeCircularReference(true); + } + private function doTestUnableToNormalizeCircularReference(bool $legacy = false) + { + $legacy ? $this->normalizer->setCircularReferenceLimit(2) : $this->createNormalizer(array(GetSetMethodNormalizer::CIRCULAR_REFERENCE_LIMIT => 2)); + $this->serializer = new Serializer(array($this->normalizer)); + $this->normalizer->setSerializer($this->serializer); + + $obj = new CircularReferenceDummy(); $this->normalizer->normalize($obj); } @@ -430,11 +484,23 @@ public function testSiblingReference() public function testCircularReferenceHandler() { - $serializer = new Serializer(array($this->normalizer)); - $this->normalizer->setSerializer($serializer); - $this->normalizer->setCircularReferenceHandler(function ($obj) { + $this->doTestCircularReferenceHandler(); + } + + public function testLegacyCircularReferenceHandler() + { + $this->doTestCircularReferenceHandler(true); + } + + private function doTestCircularReferenceHandler(bool $legacy = false) + { + $handler = function ($obj) { return \get_class($obj); - }); + }; + + $legacy ? $this->normalizer->setCircularReferenceHandler($handler) : $this->createNormalizer(array(GetSetMethodNormalizer::CIRCULAR_REFERENCE_HANDLER => $handler)); + $this->serializer = new Serializer(array($this->normalizer)); + $this->normalizer->setSerializer($this->serializer); $obj = new CircularReferenceDummy(); diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/JsonSerializableNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/JsonSerializableNormalizerTest.php index 065d92b99f05..d1628be2b539 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/JsonSerializableNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/JsonSerializableNormalizerTest.php @@ -33,9 +33,14 @@ class JsonSerializableNormalizerTest extends TestCase private $serializer; protected function setUp() + { + $this->createNormalizer(); + } + + private function createNormalizer(array $defaultContext = array()) { $this->serializer = $this->getMockBuilder(JsonSerializerNormalizer::class)->getMock(); - $this->normalizer = new JsonSerializableNormalizer(); + $this->normalizer = new JsonSerializableNormalizer(null, null, $defaultContext); $this->normalizer->setSerializer($this->serializer); } @@ -65,7 +70,23 @@ public function testNormalize() */ public function testCircularNormalize() { - $this->normalizer->setCircularReferenceLimit(1); + $this->doTestCircularNormalize(); + } + + /** + * @expectedException \Symfony\Component\Serializer\Exception\CircularReferenceException + */ + public function testLegacyCircularNormalize() + { + $this->doTestCircularNormalize(true); + } + + /** + * @expectedException \Symfony\Component\Serializer\Exception\CircularReferenceException + */ + private function doTestCircularNormalize(bool $legacy = false) + { + $legacy ? $this->normalizer->setCircularReferenceLimit(1) : $this->createNormalizer(array(JsonSerializableNormalizer::CIRCULAR_REFERENCE_LIMIT => 1)); $this->serializer ->expects($this->once()) diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php index 9f1c590a0950..9eff7914d1c8 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php @@ -17,6 +17,7 @@ use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; @@ -48,9 +49,14 @@ class ObjectNormalizerTest extends TestCase private $serializer; protected function setUp() + { + $this->createNormalizer(); + } + + private function createNormalizer(array $defaultContext = array(), ClassMetadataFactoryInterface $classMetadataFactory = null) { $this->serializer = $this->getMockBuilder(__NAMESPACE__.'\ObjectSerializerNormalizer')->getMock(); - $this->normalizer = new ObjectNormalizer(); + $this->normalizer = new ObjectNormalizer($classMetadataFactory, null, null, null, null, null, $defaultContext); $this->normalizer->setSerializer($this->serializer); } @@ -393,8 +399,20 @@ public function testMetadataAwareNameConvertorWithNotSerializedConstructorParame */ public function testCallbacks($callbacks, $value, $result, $message) { - $this->normalizer->setCallbacks($callbacks); + $this->doTestCallbacks($callbacks, $value, $result, $message); + } + + /** + * @dataProvider provideCallbacks + */ + public function testLegacyCallbacks($callbacks, $value, $result, $message) + { + $this->doTestCallbacks($callbacks, $value, $result, $message); + } + private function doTestCallbacks($callbacks, $value, $result, $message, bool $legacy = false) + { + $legacy ? $this->normalizer->setCallbacks($callbacks) : $this->createNormalizer(array(ObjectNormalizer::CALLBACKS => $callbacks)); $obj = new ObjectConstructorDummy('', $value, true); $this->assertEquals( @@ -409,7 +427,21 @@ public function testCallbacks($callbacks, $value, $result, $message) */ public function testUncallableCallbacks() { - $this->normalizer->setCallbacks(array('bar' => null)); + $this->doTestUncallableCallbacks(); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testLegacyUncallableCallbacks() + { + $this->doTestUncallableCallbacks(true); + } + + private function doTestUncallableCallbacks(bool $legacy = false) + { + $callbacks = array('bar' => null); + $legacy ? $this->normalizer->setCallbacks($callbacks) : $this->createNormalizer(array(ObjectNormalizer::CALLBACKS => $callbacks)); $obj = new ObjectConstructorDummy('baz', 'quux', true); @@ -418,7 +450,18 @@ public function testUncallableCallbacks() public function testIgnoredAttributes() { - $this->normalizer->setIgnoredAttributes(array('foo', 'bar', 'baz', 'camelCase', 'object')); + $this->doTestIgnoredAttributes(); + } + + public function testLegacyIgnoredAttributes() + { + $this->doTestIgnoredAttributes(true); + } + + private function doTestIgnoredAttributes(bool $legacy = false) + { + $ignoredAttributes = array('foo', 'bar', 'baz', 'camelCase', 'object'); + $legacy ? $this->normalizer->setIgnoredAttributes($ignoredAttributes) : $this->createNormalizer(array(ObjectNormalizer::IGNORED_ATTRIBUTES => $ignoredAttributes)); $obj = new ObjectDummy(); $obj->setFoo('foo'); @@ -433,7 +476,18 @@ public function testIgnoredAttributes() public function testIgnoredAttributesDenormalize() { - $this->normalizer->setIgnoredAttributes(array('fooBar', 'bar', 'baz')); + $this->doTestIgnoredAttributesDenormalize(); + } + + public function testLegacyIgnoredAttributesDenormalize() + { + $this->doTestIgnoredAttributesDenormalize(true); + } + + private function doTestIgnoredAttributesDenormalize(bool $legacy = false) + { + $ignoredAttributes = array('fooBar', 'bar', 'baz'); + $legacy ? $this->normalizer->setIgnoredAttributes($ignoredAttributes) : $this->createNormalizer(array(ObjectNormalizer::IGNORED_ATTRIBUTES => $ignoredAttributes)); $obj = new ObjectDummy(); $obj->setFoo('foo'); @@ -466,7 +520,7 @@ public function provideCallbacks() $this->assertInstanceOf(ObjectConstructorDummy::class, $object); $this->assertSame('bar', $attributeName); $this->assertSame('any', $format); - $this->assertArrayHasKey('circular_reference_limit', $context); + $this->assertArrayHasKey('circular_reference_limit_counters', $context); }, ), 'baz', @@ -532,9 +586,22 @@ public function testUnableToNormalizeObjectAttribute() */ public function testUnableToNormalizeCircularReference() { + $this->doTestUnableToNormalizeCircularReference(); + } + + /** + * @expectedException \Symfony\Component\Serializer\Exception\CircularReferenceException + */ + public function testLegacyUnableToNormalizeCircularReference() + { + $this->doTestUnableToNormalizeCircularReference(true); + } + + private function doTestUnableToNormalizeCircularReference(bool $legacy = false) + { + $legacy ? $this->normalizer->setCircularReferenceLimit(2) : $this->createNormalizer(array(ObjectNormalizer::CIRCULAR_REFERENCE_LIMIT => 2)); $serializer = new Serializer(array($this->normalizer)); $this->normalizer->setSerializer($serializer); - $this->normalizer->setCircularReferenceLimit(2); $obj = new CircularReferenceDummy(); @@ -558,27 +625,41 @@ public function testSiblingReference() public function testCircularReferenceHandler() { - $serializer = new Serializer(array($this->normalizer)); - $this->normalizer->setSerializer($serializer); - $this->normalizer->setCircularReferenceHandler(function ($obj) { + $this->doTestCircularReferenceHandler(); + } + + public function testLegacyCircularReferenceHandler() + { + $this->doTestCircularReferenceHandler(true); + } + + private function doTestCircularReferenceHandler(bool $legacy = false) + { + $this->createNormalizerWithCircularReferenceHandler(function ($obj) { return \get_class($obj); - }); + }, $legacy); $obj = new CircularReferenceDummy(); - $expected = array('me' => 'Symfony\Component\Serializer\Tests\Fixtures\CircularReferenceDummy'); $this->assertEquals($expected, $this->normalizer->normalize($obj)); - $this->normalizer->setCircularReferenceHandler(function ($obj, string $format, array $context) { + $this->createNormalizerWithCircularReferenceHandler(function ($obj, string $format, array $context) { $this->assertInstanceOf(CircularReferenceDummy::class, $obj); $this->assertSame('test', $format); $this->arrayHasKey('foo', $context); return \get_class($obj); - }); + }, $legacy); $this->assertEquals($expected, $this->normalizer->normalize($obj, 'test', array('foo' => 'bar'))); } + private function createNormalizerWithCircularReferenceHandler(callable $handler, bool $legacy) + { + $legacy ? $this->normalizer->setCircularReferenceHandler($handler) : $this->createNormalizer(array(ObjectNormalizer::CIRCULAR_REFERENCE_HANDLER => $handler)); + $this->serializer = new Serializer(array($this->normalizer)); + $this->normalizer->setSerializer($this->serializer); + } + public function testDenormalizeNonExistingAttribute() { $this->assertEquals( @@ -620,11 +701,16 @@ public function testNormalizeNotSerializableContext() public function testMaxDepth() { - $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); - $this->normalizer = new ObjectNormalizer($classMetadataFactory); - $serializer = new Serializer(array($this->normalizer)); - $this->normalizer->setSerializer($serializer); + $this->doTestMaxDepth(); + } + + public function testLegacyMaxDepth() + { + $this->doTestMaxDepth(true); + } + private function doTestMaxDepth(bool $legacy = false) + { $level1 = new MaxDepthDummy(); $level1->foo = 'level1'; @@ -636,7 +722,8 @@ public function testMaxDepth() $level3->foo = 'level3'; $level2->child = $level3; - $result = $serializer->normalize($level1, null, array(ObjectNormalizer::ENABLE_MAX_DEPTH => true)); + $this->createNormalizerWithMaxDepthHandler(null, $legacy); + $result = $this->serializer->normalize($level1, null, array(ObjectNormalizer::ENABLE_MAX_DEPTH => true)); $expected = array( 'bar' => null, @@ -667,14 +754,13 @@ public function testMaxDepth() ), ); - $this->normalizer->setMaxDepthHandler(function ($obj) { + $this->createNormalizerWithMaxDepthHandler(function () { return 'handler'; - }); - - $result = $serializer->normalize($level1, null, array(ObjectNormalizer::ENABLE_MAX_DEPTH => true)); + }, $legacy); + $result = $this->serializer->normalize($level1, null, array(ObjectNormalizer::ENABLE_MAX_DEPTH => true)); $this->assertEquals($expected, $result); - $this->normalizer->setMaxDepthHandler(function ($object, $parentObject, $attributeName, $format, $context) { + $this->createNormalizerWithMaxDepthHandler(function ($object, $parentObject, $attributeName, $format, $context) { $this->assertSame('level3', $object); $this->assertInstanceOf(MaxDepthDummy::class, $parentObject); $this->assertSame('foo', $attributeName); @@ -682,9 +768,23 @@ public function testMaxDepth() $this->assertArrayHasKey(ObjectNormalizer::ENABLE_MAX_DEPTH, $context); return 'handler'; - }); + }, $legacy); + $this->serializer->normalize($level1, 'test', array(ObjectNormalizer::ENABLE_MAX_DEPTH => true)); + } - $serializer->normalize($level1, 'test', array(ObjectNormalizer::ENABLE_MAX_DEPTH => true)); + private function createNormalizerWithMaxDepthHandler(callable $handler = null, bool $legacy = false) + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + if ($legacy) { + $this->createNormalizer(array(), $classMetadataFactory); + if (null !== $handler) { + $this->normalizer->setMaxDepthHandler($handler); + } + } else { + $this->createNormalizer(array(ObjectNormalizer::MAX_DEPTH_HANDLER => $handler), $classMetadataFactory); + } + $this->serializer = new Serializer(array($this->normalizer)); + $this->normalizer->setSerializer($this->serializer); } /** diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php index 444fdabc1b20..091421f07271 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php @@ -37,9 +37,14 @@ class PropertyNormalizerTest extends TestCase private $serializer; protected function setUp() + { + $this->createNormalizer(); + } + + private function createNormalizer(array $defaultContext = array()) { $this->serializer = $this->getMockBuilder('Symfony\Component\Serializer\SerializerInterface')->getMock(); - $this->normalizer = new PropertyNormalizer(); + $this->normalizer = new PropertyNormalizer(null, null, null, null, null, $defaultContext); $this->normalizer->setSerializer($this->serializer); } @@ -121,7 +126,20 @@ public function testConstructorDenormalizeWithNullArgument() */ public function testCallbacks($callbacks, $value, $result, $message) { - $this->normalizer->setCallbacks($callbacks); + $this->doTestCallbacks($callbacks, $value, $result, $message); + } + + /** + * @dataProvider provideCallbacks + */ + public function testLegacyCallbacks($callbacks, $value, $result, $message) + { + $this->doTestCallbacks($callbacks, $value, $result, $message, true); + } + + private function doTestCallbacks($callbacks, $value, $result, $message, bool $legacy = false) + { + $legacy ? $this->normalizer->setCallbacks($callbacks) : $this->createNormalizer(array(PropertyNormalizer::CALLBACKS => $callbacks)); $obj = new PropertyConstructorDummy('', $value); @@ -137,7 +155,24 @@ public function testCallbacks($callbacks, $value, $result, $message) */ public function testUncallableCallbacks() { - $this->normalizer->setCallbacks(array('bar' => null)); + $this->doTestUncallableCallbacks(); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testLegacyUncallableCallbacks() + { + $this->doTestUncallableCallbacks(true); + } + + /** + * @expectedException \InvalidArgumentException + */ + private function doTestUncallableCallbacks(bool $legacy = false) + { + $callbacks = array('bar' => null); + $legacy ? $this->normalizer->setCallbacks($callbacks) : $this->createNormalizer(array(PropertyNormalizer::CALLBACKS => $callbacks)); $obj = new PropertyConstructorDummy('baz', 'quux'); @@ -146,7 +181,18 @@ public function testUncallableCallbacks() public function testIgnoredAttributes() { - $this->normalizer->setIgnoredAttributes(array('foo', 'bar', 'camelCase')); + $this->doTestIgnoredAttributes(); + } + + public function testLegacyIgnoredAttributes() + { + $this->doTestIgnoredAttributes(true); + } + + private function doTestIgnoredAttributes(bool $legacy = false) + { + $ignoredAttributes = array('foo', 'bar', 'camelCase'); + $legacy ? $this->normalizer->setIgnoredAttributes($ignoredAttributes) : $this->createNormalizer(array(PropertyNormalizer::IGNORED_ATTRIBUTES => $ignoredAttributes)); $obj = new PropertyDummy(); $obj->foo = 'foo'; @@ -324,9 +370,22 @@ public function provideCallbacks() */ public function testUnableToNormalizeCircularReference() { - $serializer = new Serializer(array($this->normalizer)); - $this->normalizer->setSerializer($serializer); - $this->normalizer->setCircularReferenceLimit(2); + $this->doTestUnableToNormalizeCircularReference(); + } + + /** + * @expectedException \Symfony\Component\Serializer\Exception\CircularReferenceException + */ + public function testLegacyUnableToNormalizeCircularReference() + { + $this->doTestUnableToNormalizeCircularReference(true); + } + + private function doTestUnableToNormalizeCircularReference(bool $legacy = false) + { + $legacy ? $this->normalizer->setCircularReferenceLimit(2) : $this->createNormalizer(array(PropertyNormalizer::CIRCULAR_REFERENCE_LIMIT => 2)); + $this->serializer = new Serializer(array($this->normalizer)); + $this->normalizer->setSerializer($this->serializer); $obj = new PropertyCircularReferenceDummy(); @@ -350,11 +409,23 @@ public function testSiblingReference() public function testCircularReferenceHandler() { - $serializer = new Serializer(array($this->normalizer)); - $this->normalizer->setSerializer($serializer); - $this->normalizer->setCircularReferenceHandler(function ($obj) { + $this->doTestCircularReferenceHandler(); + } + + public function testLegacyCircularReferenceHandler() + { + $this->doTestCircularReferenceHandler(true); + } + + private function doTestCircularReferenceHandler(bool $legacy = false) + { + $handler = function ($obj) { return \get_class($obj); - }); + }; + $legacy ? $this->normalizer->setCircularReferenceHandler($handler) : $this->createNormalizer(array(PropertyNormalizer::CIRCULAR_REFERENCE_HANDLER => $handler)); + + $this->serializer = new Serializer(array($this->normalizer)); + $this->normalizer->setSerializer($this->serializer); $obj = new PropertyCircularReferenceDummy();