diff --git a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php index 30d6f0bb25..6ea2e950ed 100644 --- a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php +++ b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php @@ -110,6 +110,7 @@ if (!file_exists("$PHPUNIT_DIR/phpunit-$PHPUNIT_VERSION/phpunit") || md5_file(__ passthru(sprintf('\\' === DIRECTORY_SEPARATOR ? 'rmdir /S /Q %s': 'rm -rf %s', "phpunit-$PHPUNIT_VERSION.old")); } passthru("$COMPOSER create-project --no-install --prefer-dist --no-scripts --no-plugins --no-progress --ansi phpunit/phpunit phpunit-$PHPUNIT_VERSION \"$PHPUNIT_VERSION.*\""); + @copy("phpunit-$PHPUNIT_VERSION/phpunit.xsd", 'phpunit.xsd'); chdir("phpunit-$PHPUNIT_VERSION"); if ($SYMFONY_PHPUNIT_REMOVE) { passthru("$COMPOSER remove --no-update ".$SYMFONY_PHPUNIT_REMOVE); diff --git a/src/Symfony/Bridge/Twig/TwigEngine.php b/src/Symfony/Bridge/Twig/TwigEngine.php index 266d824bb4..a86b939ddd 100644 --- a/src/Symfony/Bridge/Twig/TwigEngine.php +++ b/src/Symfony/Bridge/Twig/TwigEngine.php @@ -21,6 +21,7 @@ use Twig\Environment; use Twig\Error\Error; use Twig\Error\LoaderError; use Twig\Loader\ExistsLoaderInterface; +use Twig\Loader\SourceContextLoaderInterface; use Twig\Template; /** @@ -78,19 +79,24 @@ class TwigEngine implements EngineInterface, StreamingEngineInterface $loader = $this->environment->getLoader(); - if ($loader instanceof ExistsLoaderInterface || method_exists($loader, 'exists')) { - return $loader->exists((string) $name); - } + if (1 === Environment::MAJOR_VERSION && !$loader instanceof ExistsLoaderInterface) { + try { + // cast possible TemplateReferenceInterface to string because the + // EngineInterface supports them but LoaderInterface does not + if ($loader instanceof SourceContextLoaderInterface) { + $loader->getSourceContext((string) $name); + } else { + $loader->getSource((string) $name); + } + + return true; + } catch (LoaderError $e) { + } - try { - // cast possible TemplateReferenceInterface to string because the - // EngineInterface supports them but LoaderInterface does not - $loader->getSourceContext((string) $name)->getCode(); - } catch (LoaderError $e) { return false; } - return true; + return $loader->exists((string) $name); } /** diff --git a/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php b/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php index 4bb96c96e9..662b2b9017 100644 --- a/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php +++ b/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php @@ -18,6 +18,7 @@ use Symfony\Component\HttpKernel\Log\DebugLoggerInterface; use Twig\Environment; use Twig\Error\LoaderError; use Twig\Loader\ExistsLoaderInterface; +use Twig\Loader\SourceContextLoaderInterface; /** * ExceptionController renders error or exception pages for a given @@ -120,23 +121,28 @@ class ExceptionController return sprintf('@Twig/Exception/%s.html.twig', $showException ? 'exception_full' : $name); } - // to be removed when the minimum required version of Twig is >= 3.0 + // to be removed when the minimum required version of Twig is >= 2.0 protected function templateExists($template) { $template = (string) $template; $loader = $this->twig->getLoader(); - if ($loader instanceof ExistsLoaderInterface || method_exists($loader, 'exists')) { - return $loader->exists($template); + + if (1 === Environment::MAJOR_VERSION && !$loader instanceof ExistsLoaderInterface) { + try { + if ($loader instanceof SourceContextLoaderInterface) { + $loader->getSourceContext($template); + } else { + $loader->getSource($template); + } + + return true; + } catch (LoaderError $e) { + } + + return false; } - try { - $loader->getSourceContext($template)->getCode(); - - return true; - } catch (LoaderError $e) { - } - - return false; + return $loader->exists($template); } } diff --git a/src/Symfony/Bundle/WebProfilerBundle/Controller/ExceptionController.php b/src/Symfony/Bundle/WebProfilerBundle/Controller/ExceptionController.php index fe7b3e4efd..6d4fe1e09e 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Controller/ExceptionController.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Controller/ExceptionController.php @@ -19,6 +19,7 @@ use Symfony\Component\HttpKernel\Profiler\Profiler; use Twig\Environment; use Twig\Error\LoaderError; use Twig\Loader\ExistsLoaderInterface; +use Twig\Loader\SourceContextLoaderInterface; /** * ExceptionController. @@ -118,17 +119,22 @@ class ExceptionController protected function templateExists($template) { $loader = $this->twig->getLoader(); - if ($loader instanceof ExistsLoaderInterface) { - return $loader->exists($template); + + if (1 === Environment::MAJOR_VERSION && !$loader instanceof ExistsLoaderInterface) { + try { + if ($loader instanceof SourceContextLoaderInterface) { + $loader->getSourceContext($template); + } else { + $loader->getSource($template); + } + + return true; + } catch (LoaderError $e) { + } + + return false; } - try { - $loader->getSource($template); - - return true; - } catch (LoaderError $e) { - } - - return false; + return $loader->exists($template); } } diff --git a/src/Symfony/Bundle/WebProfilerBundle/Profiler/TemplateManager.php b/src/Symfony/Bundle/WebProfilerBundle/Profiler/TemplateManager.php index b0aab8b1ec..5412a3484e 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Profiler/TemplateManager.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Profiler/TemplateManager.php @@ -99,21 +99,22 @@ class TemplateManager protected function templateExists($template) { $loader = $this->twig->getLoader(); - if ($loader instanceof ExistsLoaderInterface) { - return $loader->exists($template); - } - try { - if ($loader instanceof SourceContextLoaderInterface || method_exists($loader, 'getSourceContext')) { - $loader->getSourceContext($template); - } else { - $loader->getSource($template); + if (1 === Environment::MAJOR_VERSION && !$loader instanceof ExistsLoaderInterface) { + try { + if ($loader instanceof SourceContextLoaderInterface) { + $loader->getSourceContext($template); + } else { + $loader->getSource($template); + } + + return true; + } catch (LoaderError $e) { } - return true; - } catch (LoaderError $e) { + return false; } - return false; + return $loader->exists($template); } } diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Profiler/TemplateManagerTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/Profiler/TemplateManagerTest.php index ae4d8c7ca2..857cde44fd 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/Profiler/TemplateManagerTest.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/Profiler/TemplateManagerTest.php @@ -15,6 +15,8 @@ use Symfony\Bundle\WebProfilerBundle\Profiler\TemplateManager; use Symfony\Bundle\WebProfilerBundle\Tests\TestCase; use Symfony\Component\HttpKernel\Profiler\Profile; use Twig\Environment; +use Twig\Loader\LoaderInterface; +use Twig\Loader\SourceContextLoaderInterface; /** * Test for TemplateManager class. @@ -107,11 +109,16 @@ class TemplateManagerTest extends TestCase ->method('loadTemplate') ->willReturn('loadedTemplate'); - if (interface_exists('Twig\Loader\SourceContextLoaderInterface')) { - $loader = $this->getMockBuilder('Twig\Loader\SourceContextLoaderInterface')->getMock(); + if (Environment::MAJOR_VERSION > 1) { + $loader = $this->createMock(LoaderInterface::class); + $loader + ->expects($this->any()) + ->method('exists') + ->willReturn(true); } else { - $loader = $this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock(); + $loader = $this->createMock(SourceContextLoaderInterface::class); } + $this->twigEnvironment->expects($this->any())->method('getLoader')->willReturn($loader); return $this->twigEnvironment; diff --git a/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php b/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php index 8ad2891b19..fd810f726e 100644 --- a/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php @@ -354,7 +354,7 @@ class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterfac continue; } $version -= $this->knownTagVersions[$tag][1]; - if ((0 !== $version && 1 !== $version) || $this->knownTagVersionsTtl > $now - $this->knownTagVersions[$tag][0]) { + if ((0 !== $version && 1 !== $version) || $now - $this->knownTagVersions[$tag][0] >= $this->knownTagVersionsTtl) { // reuse previously fetched tag versions up to the ttl, unless we are storing items or a potential miss arises $fetchTagVersions = true; } else { diff --git a/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php index 490e50339a..ef939bae8c 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Cache\Tests\Adapter; use PHPUnit\Framework\MockObject\MockObject; +use Psr\Cache\CacheItemInterface; use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Cache\Adapter\TagAwareAdapter; @@ -65,6 +66,39 @@ class TagAwareAdapterTest extends AdapterTestCase $this->assertFalse($cache->prune()); } + public function testKnownTagVersionsTtl() + { + $itemsPool = new FilesystemAdapter('', 10); + $tagsPool = $this + ->getMockBuilder(AdapterInterface::class) + ->getMock(); + + $pool = new TagAwareAdapter($itemsPool, $tagsPool, 10); + + $item = $pool->getItem('foo'); + $item->tag(['baz']); + $item->expiresAfter(100); + + $tag = $this->getMockBuilder(CacheItemInterface::class)->getMock(); + $tag->expects(self::exactly(2))->method('get')->willReturn(10); + + $tagsPool->expects(self::exactly(2))->method('getItems')->willReturn([ + 'baz'.TagAwareAdapter::TAGS_PREFIX => $tag, + ]); + + $pool->save($item); + $this->assertTrue($pool->getItem('foo')->isHit()); + $this->assertTrue($pool->getItem('foo')->isHit()); + + sleep(20); + + $this->assertTrue($pool->getItem('foo')->isHit()); + + sleep(5); + + $this->assertTrue($pool->getItem('foo')->isHit()); + } + /** * @return MockObject|PruneableCacheInterface */ diff --git a/src/Symfony/Component/CssSelector/Tests/XPath/TranslatorTest.php b/src/Symfony/Component/CssSelector/Tests/XPath/TranslatorTest.php index 625096315d..a718fbd6f2 100644 --- a/src/Symfony/Component/CssSelector/Tests/XPath/TranslatorTest.php +++ b/src/Symfony/Component/CssSelector/Tests/XPath/TranslatorTest.php @@ -98,7 +98,7 @@ class TranslatorTest extends TestCase $elements = $document->xpath($translator->cssToXPath($css)); $this->assertCount(\count($elementsId), $elements); foreach ($elements as $element) { - $this->assertTrue(\in_array($element->attributes()->id, $elementsId)); + $this->assertContains((string) $element->attributes()->id, $elementsId); } } @@ -116,7 +116,7 @@ class TranslatorTest extends TestCase $this->assertCount(\count($elementsId), $elementsId); foreach ($elements as $element) { if (null !== $element->attributes()->id) { - $this->assertTrue(\in_array($element->attributes()->id, $elementsId)); + $this->assertContains((string) $element->attributes()->id, $elementsId); } } libxml_clear_errors(); @@ -137,6 +137,33 @@ class TranslatorTest extends TestCase $this->assertCount($count, $elements); } + public function testOnlyOfTypeFindsSingleChildrenOfGivenType() + { + $translator = new Translator(); + $translator->registerExtension(new HtmlExtension($translator)); + $document = new \DOMDocument(); + $document->loadHTML(<<<'HTML' + + +

+ A +

+

+ B + C +

+ + +HTML +); + + $xpath = new \DOMXPath($document); + $nodeList = $xpath->query($translator->cssToXPath('span:only-of-type')); + + $this->assertSame(1, $nodeList->length); + $this->assertSame('A', $nodeList->item(0)->textContent); + } + public function getXpathLiteralTestData() { return [ @@ -175,7 +202,7 @@ class TranslatorTest extends TestCase ['e:first-of-type', '*/e[position() = 1]'], ['e:last-of-type', '*/e[position() = last()]'], ['e:only-child', "*/*[(name() = 'e') and (last() = 1)]"], - ['e:only-of-type', 'e[last() = 1]'], + ['e:only-of-type', 'e[count(preceding-sibling::e)=0 and count(following-sibling::e)=0]'], ['e:empty', 'e[not(*) and not(string-length())]'], ['e:EmPTY', 'e[not(*) and not(string-length())]'], ['e:root', 'e[not(parent::*)]'], diff --git a/src/Symfony/Component/CssSelector/XPath/Extension/PseudoClassExtension.php b/src/Symfony/Component/CssSelector/XPath/Extension/PseudoClassExtension.php index 27fe47f9a5..288a9e695e 100644 --- a/src/Symfony/Component/CssSelector/XPath/Extension/PseudoClassExtension.php +++ b/src/Symfony/Component/CssSelector/XPath/Extension/PseudoClassExtension.php @@ -123,11 +123,13 @@ class PseudoClassExtension extends AbstractExtension */ public function translateOnlyOfType(XPathExpr $xpath) { - if ('*' === $xpath->getElement()) { + $element = $xpath->getElement(); + + if ('*' === $element) { throw new ExpressionErrorException('"*:only-of-type" is not implemented.'); } - return $xpath->addCondition('last() = 1'); + return $xpath->addCondition(sprintf('count(preceding-sibling::%s)=0 and count(following-sibling::%s)=0', $element, $element)); } /** diff --git a/src/Symfony/Component/HttpKernel/Fragment/HIncludeFragmentRenderer.php b/src/Symfony/Component/HttpKernel/Fragment/HIncludeFragmentRenderer.php index 9915e2c112..5d70c19024 100644 --- a/src/Symfony/Component/HttpKernel/Fragment/HIncludeFragmentRenderer.php +++ b/src/Symfony/Component/HttpKernel/Fragment/HIncludeFragmentRenderer.php @@ -19,6 +19,7 @@ use Symfony\Component\Templating\EngineInterface; use Twig\Environment; use Twig\Error\LoaderError; use Twig\Loader\ExistsLoaderInterface; +use Twig\Loader\SourceContextLoaderInterface; /** * Implements the Hinclude rendering strategy. @@ -135,22 +136,23 @@ class HIncludeFragmentRenderer extends RoutableFragmentRenderer } $loader = $this->templating->getLoader(); - if ($loader instanceof ExistsLoaderInterface || method_exists($loader, 'exists')) { - return $loader->exists($template); - } - try { - if (method_exists($loader, 'getSourceContext')) { - $loader->getSourceContext($template); - } else { - $loader->getSource($template); + if (1 === Environment::MAJOR_VERSION && !$loader instanceof ExistsLoaderInterface) { + try { + if ($loader instanceof SourceContextLoaderInterface) { + $loader->getSourceContext($template); + } else { + $loader->getSource($template); + } + + return true; + } catch (LoaderError $e) { } - return true; - } catch (LoaderError $e) { + return false; } - return false; + return $loader->exists($template); } /** diff --git a/src/Symfony/Component/Security/Http/Firewall/X509AuthenticationListener.php b/src/Symfony/Component/Security/Http/Firewall/X509AuthenticationListener.php index 43d4627bbb..7e785bdb58 100644 --- a/src/Symfony/Component/Security/Http/Firewall/X509AuthenticationListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/X509AuthenticationListener.php @@ -46,7 +46,7 @@ class X509AuthenticationListener extends AbstractPreAuthenticatedListener $user = $request->server->get($this->userKey); } elseif ( $request->server->has($this->credentialKey) - && preg_match('#emailAddress=(.+\@.+\.[^,/]+)($|,|/)#', $request->server->get($this->credentialKey), $matches) + && preg_match('#emailAddress=([^,/@]++@[^,/]++)#', $request->server->get($this->credentialKey), $matches) ) { $user = $matches[1]; } diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/X509AuthenticationListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/X509AuthenticationListenerTest.php index 9ada4b1b49..e35e685d5b 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/X509AuthenticationListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/X509AuthenticationListenerTest.php @@ -81,6 +81,7 @@ class X509AuthenticationListenerTest extends TestCase yield ['cert+something@example.com', 'CN=Sample certificate DN,emailAddress=cert+something@example.com']; yield ['cert+something@example.com', 'emailAddress=cert+something@example.com,CN=Sample certificate DN']; yield ['cert+something@example.com', 'emailAddress=cert+something@example.com']; + yield ['firstname.lastname@mycompany.co.uk', 'emailAddress=firstname.lastname@mycompany.co.uk,CN=Firstname.Lastname,OU=london,OU=company design and engineering,OU=Issuer London,OU=Roaming,OU=Interactive,OU=Users,OU=Standard,OU=Business,DC=england,DC=core,DC=company,DC=co,DC=uk']; } public function testGetPreAuthenticatedDataNoData() diff --git a/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php b/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php index 123b447b5f..44409d59fa 100644 --- a/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php @@ -45,8 +45,12 @@ class CsvEncoder implements EncoderInterface, DecoderInterface /** * @param array $defaultContext */ - public function __construct($defaultContext = [], string $enclosure = '"', string $escapeChar = '\\', string $keySeparator = '.', bool $escapeFormulas = false) + public function __construct($defaultContext = [], string $enclosure = '"', string $escapeChar = '', string $keySeparator = '.', bool $escapeFormulas = false) { + if ('' === $escapeChar && \PHP_VERSION_ID < 70400) { + $escapeChar = '\\'; + } + if (!\is_array($defaultContext)) { @trigger_error('Passing configuration options directly to the constructor is deprecated since Symfony 4.2, use the default context instead.', E_USER_DEPRECATED); diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index 7663e7446b..da45e30a7f 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -397,16 +397,18 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer return null; } - if ($type->isCollection() && null !== ($collectionValueType = $type->getCollectionValueType()) && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) { + $collectionValueType = $type->isCollection() ? $type->getCollectionValueType() : null; + + // Fix a collection that contains the only one element + // This is special to xml format only + if ('xml' === $format && null !== $collectionValueType && (!\is_array($data) || !\is_int(key($data)))) { + $data = [$data]; + } + + if (null !== $collectionValueType && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) { $builtinType = Type::BUILTIN_TYPE_OBJECT; $class = $collectionValueType->getClassName().'[]'; - // Fix a collection that contains the only one element - // This is special to xml format only - if ('xml' === $format && !\is_int(key($data))) { - $data = [$data]; - } - if (null !== $collectionKeyType = $type->getCollectionKeyType()) { $context['key_type'] = $collectionKeyType; } diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php index 0f93a99cd9..a4e4dfa8e7 100644 --- a/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php @@ -36,15 +36,51 @@ class CsvEncoderTest extends TestCase 'int' => 2, 'false' => false, 'true' => true, + 'int_one' => 1, + 'string_one' => '1', ]; // Check that true and false are appropriately handled - $this->assertEquals(<<<'CSV' -string,int,false,true -foo,2,0,1 + $this->assertSame($csv = <<<'CSV' +string,int,false,true,int_one,string_one +foo,2,0,1,1,1 CSV - , $this->encoder->encode($data, 'csv')); + , $this->encoder->encode($data, 'csv')); + + $this->assertSame([ + 'string' => 'foo', + 'int' => '2', + 'false' => '0', + 'true' => '1', + 'int_one' => '1', + 'string_one' => '1', + ], $this->encoder->decode($csv, 'csv')); + } + + /** + * @requires PHP 7.4 + */ + public function testDoubleQuotesAndSlashes() + { + $this->assertSame($csv = <<<'CSV' +0,1,2,3,4,5 +,"""","foo""","\""",\,foo\ + +CSV + , $this->encoder->encode($data = ['', '"', 'foo"', '\\"', '\\', 'foo\\'], 'csv')); + + $this->assertSame($data, $this->encoder->decode($csv, 'csv')); + } + + /** + * @requires PHP 7.4 + */ + public function testSingleSlash() + { + $this->assertSame($csv = "0\n\\\n", $this->encoder->encode($data = ['\\'], 'csv')); + $this->assertSame($data, $this->encoder->decode($csv, 'csv')); + $this->assertSame($data, $this->encoder->decode(trim($csv), 'csv')); } public function testSupportEncoding() diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php index ff4700cace..a45722423f 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -132,16 +132,54 @@ class AbstractObjectNormalizerTest extends TestCase $extractor = $this->getMockBuilder(PhpDocExtractor::class)->getMock(); $extractor->method('getTypes') ->will($this->onConsecutiveCalls( - [ - new Type( - 'array', - false, - null, - true, - new Type('int'), - new Type('object', false, DummyChild::class) - ), - ], + [new Type('array', false, null, true, new Type('int'), new Type('object', false, DummyChild::class))], + null + )); + + $denormalizer = new AbstractObjectNormalizerCollectionDummy(null, null, $extractor); + $arrayDenormalizer = new ArrayDenormalizerDummy(); + $serializer = new SerializerCollectionDummy([$arrayDenormalizer, $denormalizer]); + $arrayDenormalizer->setSerializer($serializer); + $denormalizer->setSerializer($serializer); + + return $denormalizer; + } + + public function testDenormalizeStringCollectionDecodedFromXmlWithOneChild() + { + $denormalizer = $this->getDenormalizerForStringCollection(); + + // if an xml-node can have children which should be deserialized as string[] + // and only one child exists + $stringCollection = $denormalizer->denormalize(['children' => 'foo'], StringCollection::class, 'xml'); + + $this->assertInstanceOf(StringCollection::class, $stringCollection); + $this->assertIsArray($stringCollection->children); + $this->assertCount(1, $stringCollection->children); + $this->assertEquals('foo', $stringCollection->children[0]); + } + + public function testDenormalizeStringCollectionDecodedFromXmlWithTwoChildren() + { + $denormalizer = $this->getDenormalizerForStringCollection(); + + // if an xml-node can have children which should be deserialized as string[] + // and only one child exists + $stringCollection = $denormalizer->denormalize(['children' => ['foo', 'bar']], StringCollection::class, 'xml'); + + $this->assertInstanceOf(StringCollection::class, $stringCollection); + $this->assertIsArray($stringCollection->children); + $this->assertCount(2, $stringCollection->children); + $this->assertEquals('foo', $stringCollection->children[0]); + $this->assertEquals('bar', $stringCollection->children[1]); + } + + private function getDenormalizerForStringCollection() + { + $extractor = $this->getMockBuilder(PhpDocExtractor::class)->getMock(); + $extractor->method('getTypes') + ->will($this->onConsecutiveCalls( + [new Type('array', false, null, true, new Type('int'), new Type('string'))], null )); @@ -258,6 +296,12 @@ class AbstractObjectNormalizerWithMetadata extends AbstractObjectNormalizer } } +class StringCollection +{ + /** @var string[] */ + public $children; +} + class DummyCollection { /** @var DummyChild[] */