Merge branch '4.2'

* 4.2:
  [serializer] validate that the specified callbacks and max_depth_handler are actually callable
  [Serializer] Respect ignored attributes in cache key of normalizer
  fix resetting the COLUMN environment variable
  Fix TestRunner compatibility to PhpUnit 8
  Fix dark themed componnents
  prevent mixup of the object to populate
This commit is contained in:
Fabien Potencier 2019-04-08 16:11:46 +02:00
commit 9d3c86fdb6
15 changed files with 178 additions and 212 deletions

View File

@ -23,6 +23,24 @@ class CommandForV5 extends \PHPUnit_TextUI_Command
*/
protected function createRunner()
{
return new TestRunnerForV5($this->arguments['loader']);
$listener = new SymfonyTestsListenerForV5();
$this->arguments['listeners'] = isset($this->arguments['listeners']) ? $this->arguments['listeners'] : array();
$registeredLocally = false;
foreach ($this->arguments['listeners'] as $registeredListener) {
if ($registeredListener instanceof SymfonyTestsListenerForV5) {
$registeredListener->globalListenerDisabled();
$registeredLocally = true;
break;
}
}
if (!$registeredLocally) {
$this->arguments['listeners'][] = $listener;
}
return parent::createRunner();
}
}

View File

@ -13,7 +13,7 @@ namespace Symfony\Bridge\PhpUnit\Legacy;
use PHPUnit\TextUI\Command as BaseCommand;
use PHPUnit\TextUI\TestRunner as BaseRunner;
use Symfony\Bridge\PhpUnit\TextUI\TestRunner;
use Symfony\Bridge\PhpUnit\SymfonyTestsListener;
/**
* {@inheritdoc}
@ -27,6 +27,24 @@ class CommandForV6 extends BaseCommand
*/
protected function createRunner(): BaseRunner
{
return new TestRunner($this->arguments['loader']);
$listener = new SymfonyTestsListener();
$this->arguments['listeners'] = isset($this->arguments['listeners']) ? $this->arguments['listeners'] : [];
$registeredLocally = false;
foreach ($this->arguments['listeners'] as $registeredListener) {
if ($registeredListener instanceof SymfonyTestsListener) {
$registeredListener->globalListenerDisabled();
$registeredLocally = true;
break;
}
}
if (!$registeredLocally) {
$this->arguments['listeners'][] = $listener;
}
return parent::createRunner();
}
}

View File

@ -1,48 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\PhpUnit\Legacy;
/**
* {@inheritdoc}
*
* @internal
*/
class TestRunnerForV5 extends \PHPUnit_TextUI_TestRunner
{
/**
* {@inheritdoc}
*/
protected function handleConfiguration(array &$arguments)
{
$listener = new SymfonyTestsListenerForV5();
$result = parent::handleConfiguration($arguments);
$arguments['listeners'] = isset($arguments['listeners']) ? $arguments['listeners'] : array();
$registeredLocally = false;
foreach ($arguments['listeners'] as $registeredListener) {
if ($registeredListener instanceof SymfonyTestsListenerForV5) {
$registeredListener->globalListenerDisabled();
$registeredLocally = true;
break;
}
}
if (!$registeredLocally) {
$arguments['listeners'][] = $listener;
}
return $result;
}
}

View File

@ -1,49 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\PhpUnit\Legacy;
use PHPUnit\TextUI\TestRunner as BaseRunner;
use Symfony\Bridge\PhpUnit\SymfonyTestsListener;
/**
* {@inheritdoc}
*
* @internal
*/
class TestRunnerForV6 extends BaseRunner
{
/**
* {@inheritdoc}
*/
protected function handleConfiguration(array &$arguments)
{
$listener = new SymfonyTestsListener();
parent::handleConfiguration($arguments);
$arguments['listeners'] = isset($arguments['listeners']) ? $arguments['listeners'] : array();
$registeredLocally = false;
foreach ($arguments['listeners'] as $registeredListener) {
if ($registeredListener instanceof SymfonyTestsListener) {
$registeredListener->globalListenerDisabled();
$registeredLocally = true;
break;
}
}
if (!$registeredLocally) {
$arguments['listeners'][] = $listener;
}
}
}

View File

@ -1,49 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\PhpUnit\Legacy;
use PHPUnit\TextUI\TestRunner as BaseRunner;
use Symfony\Bridge\PhpUnit\SymfonyTestsListener;
/**
* {@inheritdoc}
*
* @internal
*/
class TestRunnerForV7 extends BaseRunner
{
/**
* {@inheritdoc}
*/
protected function handleConfiguration(array &$arguments): void
{
$listener = new SymfonyTestsListener();
parent::handleConfiguration($arguments);
$arguments['listeners'] = isset($arguments['listeners']) ? $arguments['listeners'] : array();
$registeredLocally = false;
foreach ($arguments['listeners'] as $registeredListener) {
if ($registeredListener instanceof SymfonyTestsListener) {
$registeredListener->globalListenerDisabled();
$registeredLocally = true;
break;
}
}
if (!$registeredLocally) {
$arguments['listeners'][] = $listener;
}
}
}

View File

@ -1,26 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\PhpUnit\TextUI;
if (class_exists('PHPUnit_Runner_Version') && version_compare(\PHPUnit_Runner_Version::id(), '6.0.0', '<')) {
class_alias('Symfony\Bridge\PhpUnit\Legacy\TestRunnerForV5', 'Symfony\Bridge\PhpUnit\TextUI\TestRunner');
} elseif (version_compare(\PHPUnit\Runner\Version::id(), '7.0.0', '<')) {
class_alias('Symfony\Bridge\PhpUnit\Legacy\TestRunnerForV6', 'Symfony\Bridge\PhpUnit\TextUI\TestRunner');
} else {
class_alias('Symfony\Bridge\PhpUnit\Legacy\TestRunnerForV7', 'Symfony\Bridge\PhpUnit\TextUI\TestRunner');
}
if (false) {
class TestRunner
{
}
}

View File

@ -58,6 +58,7 @@
.message-item tbody.sf-toggle-content.sf-toggle-visible { display: table-row-group; }
td.message-bus-dispatch-caller { background: #f1f2f3; }
.theme-dark td.message-bus-dispatch-caller { background: var(--base-1); }
</style>
{% endblock %}

View File

@ -1011,6 +1011,8 @@ table.logs .metadata {
display: block;
font-size: 12px;
}
.theme-dark tr.status-error td,
.theme-dark tr.status-warning td { border-bottom: unset; border-top: unset; }
{# Doctrine panel
========================================================================= #}

View File

@ -48,6 +48,14 @@ class ApplicationTest extends TestCase
$this->colSize = getenv('COLUMNS');
}
protected function tearDown()
{
putenv($this->colSize ? 'COLUMNS='.$this->colSize : 'COLUMNS');
putenv('SHELL_VERBOSITY');
unset($_ENV['SHELL_VERBOSITY']);
unset($_SERVER['SHELL_VERBOSITY']);
}
public static function setUpBeforeClass()
{
self::$fixturesPath = realpath(__DIR__.'/Fixtures/');
@ -1749,14 +1757,6 @@ class ApplicationTest extends TestCase
$tester = new ApplicationTester($application);
$tester->run(['command' => 'foo']);
}
protected function tearDown()
{
putenv($this->colSize ? 'COLUMNS' : 'COLUMNS='.$this->colSize);
putenv('SHELL_VERBOSITY');
unset($_ENV['SHELL_VERBOSITY']);
unset($_SERVER['SHELL_VERBOSITY']);
}
}
class CustomApplication extends Application

View File

@ -33,7 +33,7 @@ class ProgressBarTest extends TestCase
protected function tearDown()
{
putenv($this->colSize ? 'COLUMNS' : 'COLUMNS='.$this->colSize);
putenv($this->colSize ? 'COLUMNS='.$this->colSize : 'COLUMNS');
}
public function testMultipleStart()

View File

@ -38,7 +38,7 @@ class SymfonyStyleTest extends TestCase
protected function tearDown()
{
putenv($this->colSize ? 'COLUMNS' : 'COLUMNS='.$this->colSize);
putenv($this->colSize ? 'COLUMNS='.$this->colSize : 'COLUMNS');
$this->command = null;
$this->tester = null;
}

View File

@ -25,6 +25,12 @@ class TerminalTest extends TestCase
$this->lineSize = getenv('LINES');
}
protected function tearDown()
{
putenv($this->colSize ? 'COLUMNS='.$this->colSize : 'COLUMNS');
putenv($this->lineSize ? 'LINES' : 'LINES='.$this->lineSize);
}
public function test()
{
putenv('COLUMNS=100');
@ -40,12 +46,6 @@ class TerminalTest extends TestCase
$this->assertSame(60, $terminal->getHeight());
}
protected function tearDown()
{
putenv($this->colSize ? 'COLUMNS' : 'COLUMNS='.$this->colSize);
putenv($this->lineSize ? 'LINES' : 'LINES='.$this->lineSize);
}
public function test_zero_values()
{
putenv('COLUMNS=0');

View File

@ -99,10 +99,14 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
$this->nameConverter = $nameConverter;
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);
if (\is_array($this->defaultContext[self::CALLBACKS] ?? null)) {
if (isset($this->defaultContext[self::CALLBACKS])) {
if (!\is_array($this->defaultContext[self::CALLBACKS])) {
throw new InvalidArgumentException(sprintf('The "%s" default context option must be an array of callables.', self::CALLBACKS));
}
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));
throw new InvalidArgumentException(sprintf('Invalid callback found for attribute "%s" in the "%s" default context option.', $attribute, self::CALLBACKS));
}
}
}
@ -385,6 +389,8 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
return $object;
}
// clean up even if no match
unset($context[static::OBJECT_TO_POPULATE]);
$constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes);
if ($constructor) {
@ -453,7 +459,7 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
throw new LogicException(sprintf('Cannot create an instance of %s from serialized data because the serializer inject in "%s" is not a denormalizer', $parameter->getClass(), self::class));
}
$parameterClass = $parameter->getClass()->getName();
$parameterData = $this->serializer->denormalize($parameterData, $parameterClass, $format, $this->createChildContext($context, $parameterName));
$parameterData = $this->serializer->denormalize($parameterData, $parameterClass, $format, $this->createChildContext($context, $parameterName, $format));
}
} catch (\ReflectionException $e) {
throw new RuntimeException(sprintf('Could not determine the class of the parameter "%s".', $parameterName), 0, $e);
@ -468,14 +474,15 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
}
/**
* @param array $parentContext
* @param string $attribute
* @param array $parentContext
* @param string $attribute Attribute name
* @param string|null $format
*
* @return array
*
* @internal
*/
protected function createChildContext(array $parentContext, $attribute)
protected function createChildContext(array $parentContext, $attribute/*, string $format = null */)
{
if (isset($parentContext[self::ATTRIBUTES][$attribute])) {
$parentContext[self::ATTRIBUTES] = $parentContext[self::ATTRIBUTES][$attribute];

View File

@ -61,6 +61,11 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = [])
{
parent::__construct($classMetadataFactory, $nameConverter, $defaultContext);
if (isset($this->defaultContext[self::MAX_DEPTH_HANDLER]) && !\is_callable($this->defaultContext[self::MAX_DEPTH_HANDLER])) {
throw new InvalidArgumentException(sprintf('The "%s" given in the default context is not callable.', self::MAX_DEPTH_HANDLER));
}
$this->defaultContext[self::EXCLUDE_FROM_CACHE_KEY] = [self::CIRCULAR_REFERENCE_LIMIT_COUNTERS];
$this->propertyTypeExtractor = $propertyTypeExtractor;
@ -89,6 +94,18 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
$context['cache_key'] = $this->getCacheKey($format, $context);
}
if (isset($context[self::CALLBACKS])) {
if (!\is_array($context[self::CALLBACKS])) {
throw new InvalidArgumentException(sprintf('The "%s" context option must be an array of callables.', self::CALLBACKS));
}
foreach ($context[self::CALLBACKS] as $attribute => $callback) {
if (!\is_callable($callback)) {
throw new InvalidArgumentException(sprintf('Invalid callback found for attribute "%s" in the "%s" context option.', $attribute, self::CALLBACKS));
}
}
}
if ($this->isCircularReference($object, $context)) {
return $this->handleCircularReference($object, $format, $context);
}
@ -98,7 +115,15 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
$attributes = $this->getAttributes($object, $format, $context);
$class = $this->objectClassResolver ? ($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;
if (isset($context[self::MAX_DEPTH_HANDLER])) {
$maxDepthHandler = $context[self::MAX_DEPTH_HANDLER];
if (!\is_callable($maxDepthHandler)) {
throw new InvalidArgumentException(sprintf('The "%s" given in the context is not callable.', self::MAX_DEPTH_HANDLER));
}
} else {
// already validated in constructor resp by type declaration of setMaxDepthHandler
$maxDepthHandler = $this->defaultContext[self::MAX_DEPTH_HANDLER] ?? $this->maxDepthHandler;
}
foreach ($attributes as $attribute) {
$maxDepthReached = false;
@ -131,7 +156,7 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
throw new LogicException(sprintf('Cannot normalize attribute "%s" because the injected serializer is not a normalizer', $attribute));
}
$data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $this->createChildContext($context, $attribute)), $class, $format, $context);
$data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $this->createChildContext($context, $attribute, $format)), $class, $format, $context);
}
return $data;
@ -187,21 +212,17 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
return $allowedAttributes;
}
if ($context[self::ATTRIBUTES] ?? $this->defaultContext[self::ATTRIBUTES] ?? false) {
return $this->extractAttributes($object, $format, $context);
}
if (isset($this->attributesCache[$class])) {
return $this->attributesCache[$class];
}
$attributes = $this->extractAttributes($object, $format, $context);
if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForMappedObject($object)) {
array_unshift($attributes, $mapping->getTypeProperty());
}
return $this->attributesCache[$class] = $attributes;
if ($context['cache_key']) {
$this->attributesCache[$key] = $attributes;
}
return $attributes;
}
/**
@ -356,7 +377,7 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
throw new LogicException(sprintf('Cannot denormalize attribute "%s" for class "%s" because injected serializer is not a denormalizer', $attribute, $class));
}
$childContext = $this->createChildContext($context, $attribute);
$childContext = $this->createChildContext($context, $attribute, $format);
if ($this->serializer->supportsDenormalization($data, $class, $format, $childContext)) {
return $this->serializer->denormalize($data, $class, $format, $childContext);
}
@ -486,7 +507,32 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
}
/**
* Gets the cache key to use.
* Overwritten to update the cache key for the child.
*
* We must not mix up the attribute cache between parent and children.
*
* {@inheritdoc}
*/
protected function createChildContext(array $parentContext, $attribute/*, string $format = null */)
{
if (\func_num_args() >= 3) {
$format = \func_get_arg(2);
} else {
// will be deprecated in version 4
$format = null;
}
$context = parent::createChildContext($parentContext, $attribute, $format);
// format is already included in the cache_key of the parent.
$context['cache_key'] = $this->getCacheKey($format, $context);
return $context;
}
/**
* Builds the cache key for the attributes cache.
*
* The key must be different for every option in the context that could change which attributes should be handled.
*
* @return bool|string
*/
@ -496,9 +542,14 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
unset($context[$key]);
}
unset($context[self::EXCLUDE_FROM_CACHE_KEY]);
unset($context['cache_key']); // avoid artificially different keys
try {
return md5($format.serialize($context));
return md5($format.serialize([
'context' => $context,
'ignored' => $this->ignoredAttributes,
'camelized' => $this->camelizedAttributes,
]));
} catch (\Exception $exception) {
// The context cannot be serialized, skip the cache
return false;

View File

@ -394,6 +394,30 @@ class ObjectNormalizerTest extends TestCase
);
}
public function testObjectToPopulateNoMatch()
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$this->normalizer = new ObjectNormalizer($classMetadataFactory, null, null, new PhpDocExtractor());
new Serializer([$this->normalizer]);
$objectToPopulate = new ObjectInner();
$objectToPopulate->foo = 'foo';
$outer = $this->normalizer->denormalize([
'foo' => 'foo',
'inner' => [
'bar' => 'bar',
],
], ObjectOuter::class, null, [ObjectNormalizer::OBJECT_TO_POPULATE => $objectToPopulate]);
$this->assertInstanceOf(ObjectOuter::class, $outer);
$inner = $outer->getInner();
$this->assertInstanceOf(ObjectInner::class, $inner);
$this->assertNotSame($objectToPopulate, $inner);
$this->assertSame('bar', $inner->bar);
$this->assertNull($inner->foo);
}
/**
* @dataProvider provideCallbacks
*/
@ -472,6 +496,16 @@ class ObjectNormalizerTest extends TestCase
['fooBar' => 'foobar'],
$this->normalizer->normalize($obj, 'any')
);
$this->normalizer->setIgnoredAttributes(['foo', 'baz', 'camelCase', 'object']);
$this->assertEquals(
[
'fooBar' => 'foobar',
'bar' => 'bar',
],
$this->normalizer->normalize($obj, 'any')
);
}
public function testIgnoredAttributesDenormalize()
@ -781,7 +815,11 @@ class ObjectNormalizerTest extends TestCase
$this->normalizer->setMaxDepthHandler($handler);
}
} else {
$this->createNormalizer([ObjectNormalizer::MAX_DEPTH_HANDLER => $handler], $classMetadataFactory);
$context = [];
if (null !== $handler) {
$context[ObjectNormalizer::MAX_DEPTH_HANDLER] = $handler;
}
$this->createNormalizer($context, $classMetadataFactory);
}
$this->serializer = new Serializer([$this->normalizer]);
$this->normalizer->setSerializer($this->serializer);
@ -1208,6 +1246,9 @@ class ObjectOuter
{
public $foo;
public $bar;
/**
* @var ObjectInner
*/
private $inner;
private $date;