Skip to content

[Serializer] make XmlEncoder stateless thus reentrant #38534

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 35 additions & 45 deletions src/Symfony/Component/Serializer/Encoder/XmlEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
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>
Expand Down Expand Up @@ -68,13 +66,6 @@ class XmlEncoder implements EncoderInterface, DecoderInterface, NormalizationAwa
self::TYPE_CAST_ATTRIBUTES => true,
];

/**
* @var \DOMDocument
*/
private $dom;
private $format;
private $context;

/**
* @param array $defaultContext
*/
Expand Down Expand Up @@ -107,19 +98,17 @@ public function encode($data, $format, array $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);
$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($ignorePiNode ? $this->dom->documentElement : null);
return $dom->saveXML($ignorePiNode ? $dom->documentElement : null);
}

/**
Expand Down Expand Up @@ -242,7 +231,7 @@ public function getRootNodeName()
final protected function appendXMLString(\DOMNode $node, string $val): bool
{
if ('' !== $val) {
$frag = $this->dom->createDocumentFragment();
$frag = $node->ownerDocument->createDocumentFragment();
$frag->appendXML($val);
$node->appendChild($frag);

Expand All @@ -254,15 +243,15 @@ final protected function appendXMLString(\DOMNode $node, string $val): bool

final protected function appendText(\DOMNode $node, string $val): bool
{
$nodeText = $this->dom->createTextNode($val);
$nodeText = $node->ownerDocument->createTextNode($val);
$node->appendChild($nodeText);

return true;
}

final protected function appendCData(\DOMNode $node, string $val): bool
{
$nodeText = $this->dom->createCDATASection($val);
$nodeText = $node->ownerDocument->createCDATASection($val);
$node->appendChild($nodeText);

return true;
Expand All @@ -284,7 +273,7 @@ final protected function appendDocumentFragment(\DOMNode $node, $fragment): bool

final protected function appendComment(\DOMNode $node, string $data): bool
{
$node->appendChild($this->dom->createComment($data));
$node->appendChild($node->ownerDocument->createComment($data));

return true;
}
Expand Down Expand Up @@ -412,22 +401,22 @@ private function parseXmlValue(\DOMNode $node, array $context = [])
*
* @throws NotEncodableValueException
*/
private function buildXml(\DOMNode $parentNode, $data, string $xmlRootNodeName = null): bool
private function buildXml(\DOMNode $parentNode, $data, string $format, array $context, 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];
$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 (str_starts_with($key, '@') && $this->isElementNameValid($attributeName = substr($key, 1))) {
if (!is_scalar($data)) {
$data = $this->serializer->normalize($data, $this->format, $this->context);
$data = $this->serializer->normalize($data, $format, $context);
}
$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);
Expand All @@ -441,15 +430,15 @@ private function buildXml(\DOMNode $parentNode, $data, string $xmlRootNodeName =
* 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);
$append = $this->appendNode($parentNode, $data, $format, $context, 'item', $key);
} elseif (null !== $data || !$removeEmptyTags) {
$append = $this->appendNode($parentNode, $data, $key);
$append = $this->appendNode($parentNode, $data, $format, $context, $key);
}
}

Expand All @@ -461,20 +450,20 @@ private function buildXml(\DOMNode $parentNode, $data, string $xmlRootNodeName =
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);
$data = $this->serializer->normalize($data, $format, $context);
if (null !== $data && !is_scalar($data)) {
return $this->buildXml($parentNode, $data, $xmlRootNodeName);
return $this->buildXml($parentNode, $data, $format, $context, $xmlRootNodeName);
}

// top level data object was normalized into a scalar
if (!$parentNode->parentNode->parentNode) {
$root = $parentNode->parentNode;
$root->removeChild($parentNode);

return $this->appendNode($root, $data, $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))));
Expand All @@ -485,13 +474,14 @@ private function buildXml(\DOMNode $parentNode, $data, string $xmlRootNodeName =
*
* @param array|object $data
*/
private function appendNode(\DOMNode $parentNode, $data, string $nodeName, string $key = null): bool
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);
Expand All @@ -505,32 +495,32 @@ private function appendNode(\DOMNode $parentNode, $data, string $nodeName, strin
*/
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.
*
* @throws NotEncodableValueException
*/
private function selectNodeType(\DOMNode $node, $val): bool
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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Tests\Fixtures\Dummy;
use Symfony\Component\Serializer\Tests\Fixtures\EnvelopedMessage;
use Symfony\Component\Serializer\Tests\Fixtures\EnvelopedMessageNormalizer;
use Symfony\Component\Serializer\Tests\Fixtures\EnvelopeNormalizer;
use Symfony\Component\Serializer\Tests\Fixtures\EnvelopeObject;
use Symfony\Component\Serializer\Tests\Fixtures\NormalizableTraversableDummy;
use Symfony\Component\Serializer\Tests\Fixtures\ScalarDummy;

Expand Down Expand Up @@ -850,6 +854,23 @@ public function testNotEncodableValueExceptionMessageForAResource()
(new XmlEncoder())->encode(tmpfile(), 'xml');
}

public function testReentrantXmlEncoder()
{
$envelope = new EnvelopeObject();
$message = new EnvelopedMessage();
$message->text = 'Symfony is great';
$envelope->message = $message;

$encoder = $this->createXmlEncoderWithEnvelopeNormalizer();
$expected = <<<'XML'
<?xml version="1.0"?>
<response><message>PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxyZXNwb25zZT48dGV4dD5TeW1mb255IGlzIGdyZWF0PC90ZXh0PjwvcmVzcG9uc2U+Cg==</message></response>

XML;

$this->assertSame($expected, $encoder->encode($envelope, 'xml'));
}

public function testEncodeComment()
{
$expected = <<<'XML'
Expand Down Expand Up @@ -921,6 +942,21 @@ private function doTestEncodeWithoutComment(bool $legacy = false)
$this->assertEquals($expected, $encoder->encode($data, 'xml'));
}

private function createXmlEncoderWithEnvelopeNormalizer(): XmlEncoder
{
$normalizers = [
$envelopeNormalizer = new EnvelopeNormalizer(),
new EnvelopedMessageNormalizer(),
];

$encoder = new XmlEncoder();
$serializer = new Serializer($normalizers, ['xml' => $encoder]);
$encoder->setSerializer($serializer);
$envelopeNormalizer->setSerializer($serializer);

return $encoder;
}

private function createXmlEncoderWithDateTimeNormalizer(): XmlEncoder
{
$encoder = new XmlEncoder();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Serializer\Tests\Fixtures;

use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

/**
* @author Karoly Gossler <connor@connor.hu>
*/
class EnvelopeNormalizer implements NormalizerInterface
{
private $serializer;

public function normalize($envelope, $format = null, array $context = [])
{
$xmlContent = $this->serializer->serialize($envelope->message, 'xml');

$encodedContent = base64_encode($xmlContent);

return [
'message' => $encodedContent,
];
}

public function supportsNormalization($data, $format = null)
{
return $data instanceof EnvelopeObject;
}

public function setSerializer($serializer)
{
$this->serializer = $serializer;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?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 Karoly Gossler <connor@connor.hu>
*/
class EnvelopeObject
{
public $message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?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 Karoly Gossler <connor@connor.hu>
*/
class EnvelopedMessage
{
public $text;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?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\NormalizerInterface;

/**
* @author Karoly Gossler <connor@connor.hu>
*/
class EnvelopedMessageNormalizer implements NormalizerInterface
{
public function normalize($message, $format = null, array $context = [])
{
return [
'text' => $message->text,
];
}

public function supportsNormalization($data, $format = null)
{
return $data instanceof EnvelopedMessage;
}
}