Merge branch '4.4' into 5.0

* 4.4: (21 commits)
  fix merge
  CS
  [FrameworkBundle][ContainerLintCommand] Improve messages when the kernel or the container is not supported
  [Serializer] Skip uninitialized (PHP 7.4) properties in PropertyNormalizer and ObjectNormalizer
  stop using deprecated Doctrine persistence classes
  [Cache] Fix wrong classname in deprecation message
  Fix regex lookahead syntax in ApplicationTest
  Fixed syntax in comment
  [SecurityBundle][FirewallMap] Remove unused property
  [Messenger][AMQP] Use delivery_mode=2 by default
  [FrameworkBundle][DependencyInjection] Skip removed ids in the lint container command and its associated pass
  [SECURITY] Revert "AbstractAuthenticationListener.php error instead info. Rebase of #28462"
  [FrameworkBundle][Secrets] Hook configured local dotenv file
  [DI] Improve performance of processDefinition
  fix redis multi host dsn not recognized
  fix constructor argument type declaration
  Fix invalid Windows path normalization
  [Validator][ConstraintValidator] Safe fail on invalid timezones
  [DoctrineBridge] Fixed submitting invalid ids when using queries with limit
  [FrameworkBundle] Add info & example to auto_mapping config
  ...
This commit is contained in:
Nicolas Grekas 2019-12-16 11:47:49 +01:00
commit 525d7bf716
39 changed files with 331 additions and 76 deletions

View File

@ -50,6 +50,21 @@ class ORMQueryBuilderLoader implements EntityLoaderInterface
*/
public function getEntitiesByIds(string $identifier, array $values)
{
if (null !== $this->queryBuilder->getMaxResults() || null !== $this->queryBuilder->getFirstResult()) {
// an offset or a limit would apply on results including the where clause with submitted id values
// that could make invalid choices valid
$choices = [];
$metadata = $this->queryBuilder->getEntityManager()->getClassMetadata(current($this->queryBuilder->getRootEntities()));
foreach ($this->getEntities() as $entity) {
if (\in_array(current($metadata->getIdentifierValues($entity)), $values, true)) {
$choices[] = $entity;
}
}
return $choices;
}
$qb = clone $this->queryBuilder;
$alias = current($qb->getRootAliases());
$parameter = 'ORMQueryBuilderLoader_getEntitiesByIds_'.$identifier;

View File

@ -953,6 +953,31 @@ class EntityTypeTest extends BaseTypeTest
$this->assertNull($field->getData());
}
public function testDisallowChoicesThatAreNotIncludedQueryBuilderSingleIdentifierWithLimit()
{
$entity1 = new SingleIntIdEntity(1, 'Foo');
$entity2 = new SingleIntIdEntity(2, 'Bar');
$entity3 = new SingleIntIdEntity(3, 'Baz');
$this->persist([$entity1, $entity2, $entity3]);
$repository = $this->em->getRepository(self::SINGLE_IDENT_CLASS);
$field = $this->factory->createNamed('name', static::TESTED_TYPE, null, [
'em' => 'default',
'class' => self::SINGLE_IDENT_CLASS,
'query_builder' => $repository->createQueryBuilder('e')
->where('e.id IN (1, 2, 3)')
->setMaxResults(1),
'choice_label' => 'name',
]);
$field->submit('3');
$this->assertFalse($field->isSynchronized());
$this->assertNull($field->getData());
}
public function testDisallowChoicesThatAreNotIncludedQueryBuilderSingleAssocIdentifier()
{
$innerEntity1 = new SingleIntIdNoToStringEntity(1, 'InFoo');

View File

@ -14,13 +14,17 @@ namespace Symfony\Bundle\FrameworkBundle\Command;
use Symfony\Component\Config\ConfigCache;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\DependencyInjection\Compiler\CheckTypeDeclarationsPass;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
use Symfony\Component\HttpKernel\Kernel;
final class ContainerLintCommand extends Command
{
@ -47,13 +51,18 @@ final class ContainerLintCommand extends Command
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$container = $this->getContainerBuilder();
$io = new SymfonyStyle($input, $output);
$errorIo = $io->getErrorStyle();
try {
$container = $this->getContainerBuilder();
} catch (RuntimeException $e) {
$errorIo->error($e->getMessage());
return 2;
}
$container->setParameter('container.build_hash', 'lint_container');
$container->setParameter('container.build_time', time());
$container->setParameter('container.build_id', 'lint_container');
$container->addCompilerPass(new CheckTypeDeclarationsPass(true), PassConfig::TYPE_AFTER_REMOVING, -100);
$container->compile();
@ -67,22 +76,44 @@ final class ContainerLintCommand extends Command
}
$kernel = $this->getApplication()->getKernel();
$kernelContainer = $kernel->getContainer();
if (!$kernel->isDebug() || !(new ConfigCache($kernelContainer->getParameter('debug.container.dump'), true))->isFresh()) {
if (!$kernel instanceof Kernel) {
throw new RuntimeException(sprintf('This command does not support the application kernel: "%s" does not extend "%s".', \get_class($kernel), Kernel::class));
}
if (!$kernel->isDebug() || !(new ConfigCache($kernel->getContainer()->getParameter('debug.container.dump'), true))->isFresh()) {
$buildContainer = \Closure::bind(function (): ContainerBuilder {
$this->initializeBundles();
return $this->buildContainer();
}, $kernel, \get_class($kernel));
$container = $buildContainer();
$skippedIds = [];
} else {
(new XmlFileLoader($container = new ContainerBuilder($parameterBag = new EnvPlaceholderParameterBag()), new FileLocator()))->load($kernel->getContainer()->getParameter('debug.container.dump'));
if (!$kernelContainer instanceof Container) {
throw new RuntimeException(sprintf('This command does not support the application container: "%s" does not extend "%s".', \get_class($kernelContainer), Container::class));
}
(new XmlFileLoader($container = new ContainerBuilder($parameterBag = new EnvPlaceholderParameterBag()), new FileLocator()))->load($kernelContainer->getParameter('debug.container.dump'));
$refl = new \ReflectionProperty($parameterBag, 'resolved');
$refl->setAccessible(true);
$refl->setValue($parameterBag, true);
$passConfig = $container->getCompilerPassConfig();
$passConfig->setRemovingPasses([]);
$passConfig->setAfterRemovingPasses([]);
$skippedIds = $kernelContainer->getRemovedIds();
}
$container->setParameter('container.build_hash', 'lint_container');
$container->setParameter('container.build_id', 'lint_container');
$container->addCompilerPass(new CheckTypeDeclarationsPass(true, $skippedIds), PassConfig::TYPE_AFTER_REMOVING, -100);
return $this->containerBuilder = $container;
}
}

View File

@ -130,7 +130,7 @@ class Configuration implements ConfigurationInterface
->canBeDisabled()
->children()
->scalarNode('vault_directory')->defaultValue('%kernel.project_dir%/config/secrets/%kernel.environment%')->cannotBeEmpty()->end()
->scalarNode('local_dotenv_file')->defaultValue('%kernel.project_dir%/.env.local')->end()
->scalarNode('local_dotenv_file')->defaultValue('%kernel.project_dir%/.env.%kernel.environment%.local')->end()
->scalarNode('decryption_env_var')->defaultValue('base64:default::SYMFONY_DECRYPTION_SECRET')->end()
->end()
->end()
@ -724,6 +724,11 @@ class Configuration implements ConfigurationInterface
->end()
->end()
->arrayNode('auto_mapping')
->info('A collection of namespaces for which auto-mapping will be enabled.')
->example([
'App\\Entity\\' => [],
'App\\WithSpecificLoaders\\' => ['validator.property_info_loader'],
])
->useAttributeAsKey('namespace')
->normalizeKeys(false)
->beforeNormalization()

View File

@ -1353,7 +1353,9 @@ class FrameworkExtension extends Extension
$container->getDefinition('secrets.vault')->replaceArgument(0, $config['vault_directory']);
if (!$config['local_dotenv_file']) {
if ($config['local_dotenv_file']) {
$container->getDefinition('secrets.local_vault')->replaceArgument(0, $config['local_dotenv_file']);
} else {
$container->removeDefinition('secrets.local_vault');
}

View File

@ -7,12 +7,12 @@
<services>
<service id="secrets.vault" class="Symfony\Bundle\FrameworkBundle\Secrets\SodiumVault">
<tag name="container.env_var_loader" />
<argument>%kernel.project_dir%/config/secrets/%kernel.environment%</argument>
<argument>%env(base64:default::SYMFONY_DECRYPTION_SECRET)%</argument>
<argument />
<argument />
</service>
<service id="secrets.local_vault" class="Symfony\Bundle\FrameworkBundle\Secrets\DotenvVault">
<argument>%kernel.project_dir%/.env.local</argument>
<argument />
</service>
</services>
</container>

View File

@ -503,7 +503,7 @@ class ConfigurationTest extends TestCase
'secrets' => [
'enabled' => true,
'vault_directory' => '%kernel.project_dir%/config/secrets/%kernel.environment%',
'local_dotenv_file' => '%kernel.project_dir%/.env.local',
'local_dotenv_file' => '%kernel.project_dir%/.env.%kernel.environment%.local',
'decryption_env_var' => 'base64:default::SYMFONY_DECRYPTION_SECRET',
],
];

View File

@ -26,13 +26,11 @@ class FirewallMap implements FirewallMapInterface
{
private $container;
private $map;
private $contexts;
public function __construct(ContainerInterface $container, iterable $map)
{
$this->container = $container;
$this->map = $map;
$this->contexts = new \SplObjectStorage();
}
public function getListeners(Request $request)

View File

@ -616,7 +616,7 @@ class ApplicationTest extends TestCase
$this->assertRegExp(sprintf('/Command "%s" is not defined./', $commandName), $e->getMessage(), '->find() throws a CommandNotFoundException if command does not exist, with alternatives');
$this->assertRegExp('/afoobar1/', $e->getMessage(), '->find() throws a CommandNotFoundException if command does not exist, with alternative : "afoobar1"');
$this->assertRegExp('/foo:bar1/', $e->getMessage(), '->find() throws a CommandNotFoundException if command does not exist, with alternative : "foo:bar1"');
$this->assertNotRegExp('/foo:bar(?>!1)/', $e->getMessage(), '->find() throws a CommandNotFoundException if command does not exist, without "foo:bar" alternative');
$this->assertNotRegExp('/foo:bar(?!1)/', $e->getMessage(), '->find() throws a CommandNotFoundException if command does not exist, without "foo:bar" alternative');
}
}

View File

@ -42,16 +42,19 @@ final class CheckTypeDeclarationsPass extends AbstractRecursivePass
private const SCALAR_TYPES = ['int', 'float', 'bool', 'string'];
private $autoload;
private $skippedIds;
private $expressionLanguage;
/**
* @param bool $autoload Whether services who's class in not loaded should be checked or not.
* Defaults to false to save loading code during compilation.
* @param bool $autoload Whether services who's class in not loaded should be checked or not.
* Defaults to false to save loading code during compilation.
* @param array $skippedIds An array indexed by the service ids to skip
*/
public function __construct(bool $autoload = false)
public function __construct(bool $autoload = false, array $skippedIds = [])
{
$this->autoload = $autoload;
$this->skippedIds = $skippedIds;
}
/**
@ -59,6 +62,10 @@ final class CheckTypeDeclarationsPass extends AbstractRecursivePass
*/
protected function processValue($value, $isRoot = false)
{
if (isset($this->skippedIds[$this->currentId])) {
return $value;
}
if (!$value instanceof Definition || $value->hasErrors()) {
return parent::processValue($value, $isRoot);
}

View File

@ -63,9 +63,10 @@ class ResolveInstanceofConditionalsPass implements CompilerPassInterface
$instanceofTags = [];
$instanceofCalls = [];
$instanceofBindings = [];
$reflectionClass = null;
foreach ($conditionals as $interface => $instanceofDefs) {
if ($interface !== $class && (!$container->getReflectionClass($class, false))) {
if ($interface !== $class && !(null === $reflectionClass ? $reflectionClass = ($container->getReflectionClass($class, false) ?: false) : $reflectionClass)) {
continue;
}

View File

@ -669,4 +669,16 @@ class CheckTypeDeclarationsPassTest extends TestCase
$this->addToAssertionCount(1);
}
public function testProcessSkipSkippedIds()
{
$container = new ContainerBuilder();
$container
->register('foobar', BarMethodCall::class)
->addMethodCall('setArray', ['a string']);
(new CheckTypeDeclarationsPass(true, ['foobar' => true]))->process($container);
$this->addToAssertionCount(1);
}
}

View File

@ -50,13 +50,13 @@ class SessionHandlerFactory
case 0 === strpos($connection, 'file://'):
return new StrictSessionHandler(new NativeFileSessionHandler(substr($connection, 7)));
case 0 === strpos($connection, 'redis://'):
case 0 === strpos($connection, 'rediss://'):
case 0 === strpos($connection, 'memcached://'):
case 0 === strpos($connection, 'redis:'):
case 0 === strpos($connection, 'rediss:'):
case 0 === strpos($connection, 'memcached:'):
if (!class_exists(AbstractAdapter::class)) {
throw new InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require symfony/cache".', $connection));
}
$handlerClass = 0 === strpos($connection, 'memcached://') ? MemcachedSessionHandler::class : RedisSessionHandler::class;
$handlerClass = 0 === strpos($connection, 'memcached:') ? MemcachedSessionHandler::class : RedisSessionHandler::class;
$connection = AbstractAdapter::createConnection($connection, ['lazy' => true]);
return new $handlerClass($connection);

View File

@ -28,7 +28,7 @@ interface MessageSubscriberInterface extends MessageHandlerInterface
* It can also change the priority per classes.
*
* yield FirstMessage::class => ['priority' => 0];
* yield SecondMessage::class => ['priority => -10];
* yield SecondMessage::class => ['priority' => -10];
*
* It can also specify a method, a priority, a bus and/or a transport per message:
*

View File

@ -227,7 +227,7 @@ class ConnectionTest extends TestCase
);
$amqpExchange->expects($this->once())->method('declareExchange');
$amqpExchange->expects($this->once())->method('publish')->with('body', null, AMQP_NOPARAM, ['headers' => []]);
$amqpExchange->expects($this->once())->method('publish')->with('body', null, AMQP_NOPARAM, ['headers' => [], 'delivery_mode' => 2]);
$amqpQueue->expects($this->once())->method('declareQueue');
$amqpQueue->expects($this->once())->method('bind')->with(self::DEFAULT_EXCHANGE_NAME, null);
@ -250,7 +250,7 @@ class ConnectionTest extends TestCase
$factory->method('createQueue')->will($this->onConsecutiveCalls($amqpQueue0, $amqpQueue1));
$amqpExchange->expects($this->once())->method('declareExchange');
$amqpExchange->expects($this->once())->method('publish')->with('body', 'routing_key', AMQP_NOPARAM, ['headers' => []]);
$amqpExchange->expects($this->once())->method('publish')->with('body', 'routing_key', AMQP_NOPARAM, ['headers' => [], 'delivery_mode' => 2]);
$amqpQueue0->expects($this->once())->method('declareQueue');
$amqpQueue0->expects($this->exactly(2))->method('bind')->withConsecutive(
[self::DEFAULT_EXCHANGE_NAME, 'binding_key0'],
@ -400,7 +400,7 @@ class ConnectionTest extends TestCase
$delayQueue->expects($this->once())->method('declareQueue');
$delayQueue->expects($this->once())->method('bind')->with('delays', 'delay_messages__5000');
$delayExchange->expects($this->once())->method('publish')->with('{}', 'delay_messages__5000', AMQP_NOPARAM, ['headers' => ['x-some-headers' => 'foo']]);
$delayExchange->expects($this->once())->method('publish')->with('{}', 'delay_messages__5000', AMQP_NOPARAM, ['headers' => ['x-some-headers' => 'foo'], 'delivery_mode' => 2]);
$connection = Connection::fromDsn('amqp://localhost', [], $factory);
$connection->publish('{}', ['x-some-headers' => 'foo'], 5000);
@ -442,7 +442,7 @@ class ConnectionTest extends TestCase
$delayQueue->expects($this->once())->method('declareQueue');
$delayQueue->expects($this->once())->method('bind')->with('delays', 'delay_messages__120000');
$delayExchange->expects($this->once())->method('publish')->with('{}', 'delay_messages__120000', AMQP_NOPARAM, ['headers' => []]);
$delayExchange->expects($this->once())->method('publish')->with('{}', 'delay_messages__120000', AMQP_NOPARAM, ['headers' => [], 'delivery_mode' => 2]);
$connection->publish('{}', [], 120000);
}
@ -474,12 +474,27 @@ class ConnectionTest extends TestCase
$amqpExchange = $this->createMock(\AMQPExchange::class)
);
$amqpExchange->expects($this->once())->method('publish')->with('body', null, AMQP_NOPARAM, ['headers' => ['Foo' => 'X', 'Bar' => 'Y']]);
$amqpExchange->expects($this->once())->method('publish')->with('body', null, AMQP_NOPARAM, ['headers' => ['Foo' => 'X', 'Bar' => 'Y'], 'delivery_mode' => 2]);
$connection = Connection::fromDsn('amqp://localhost', [], $factory);
$connection->publish('body', ['Foo' => 'X'], 0, new AmqpStamp(null, AMQP_NOPARAM, ['headers' => ['Bar' => 'Y']]));
}
public function testAmqpStampDelireryModeIsUsed()
{
$factory = new TestAmqpFactory(
$this->createMock(\AMQPConnection::class),
$this->createMock(\AMQPChannel::class),
$this->createMock(\AMQPQueue::class),
$amqpExchange = $this->createMock(\AMQPExchange::class)
);
$amqpExchange->expects($this->once())->method('publish')->with('body', null, AMQP_NOPARAM, ['headers' => [], 'delivery_mode' => 1]);
$connection = Connection::fromDsn('amqp://localhost', [], $factory);
$connection->publish('body', [], 0, new AmqpStamp(null, AMQP_NOPARAM, ['delivery_mode' => 1]));
}
public function testItCanPublishWithTheDefaultRoutingKey()
{
$factory = new TestAmqpFactory(
@ -546,7 +561,7 @@ class ConnectionTest extends TestCase
$delayQueue->expects($this->once())->method('declareQueue');
$delayQueue->expects($this->once())->method('bind')->with('delays', 'delay_messages_routing_key_120000');
$delayExchange->expects($this->once())->method('publish')->with('{}', 'delay_messages_routing_key_120000', AMQP_NOPARAM, ['headers' => []]);
$delayExchange->expects($this->once())->method('publish')->with('{}', 'delay_messages_routing_key_120000', AMQP_NOPARAM, ['headers' => [], 'delivery_mode' => 2]);
$connection->publish('{}', [], 120000, new AmqpStamp('routing_key'));
}

View File

@ -230,6 +230,7 @@ class Connection
{
$attributes = $amqpStamp ? $amqpStamp->getAttributes() : [];
$attributes['headers'] = array_merge($attributes['headers'] ?? [], $headers);
$attributes['delivery_mode'] = $attributes['delivery_mode'] ?? 2;
$exchange->publish(
$body,

View File

@ -76,17 +76,13 @@ class AccessDecisionManager implements AccessDecisionManagerInterface
$deny = 0;
foreach ($this->voters as $voter) {
$result = $voter->vote($token, $object, $attributes);
switch ($result) {
case VoterInterface::ACCESS_GRANTED:
return true;
case VoterInterface::ACCESS_DENIED:
++$deny;
if (VoterInterface::ACCESS_GRANTED === $result) {
return true;
}
break;
default:
break;
if (VoterInterface::ACCESS_DENIED === $result) {
++$deny;
}
}
@ -118,16 +114,10 @@ class AccessDecisionManager implements AccessDecisionManagerInterface
foreach ($this->voters as $voter) {
$result = $voter->vote($token, $object, $attributes);
switch ($result) {
case VoterInterface::ACCESS_GRANTED:
++$grant;
break;
case VoterInterface::ACCESS_DENIED:
++$deny;
break;
if (VoterInterface::ACCESS_GRANTED === $result) {
++$grant;
} elseif (VoterInterface::ACCESS_DENIED === $result) {
++$deny;
}
}
@ -159,17 +149,12 @@ class AccessDecisionManager implements AccessDecisionManagerInterface
foreach ($attributes as $attribute) {
$result = $voter->vote($token, $object, [$attribute]);
switch ($result) {
case VoterInterface::ACCESS_GRANTED:
++$grant;
if (VoterInterface::ACCESS_DENIED === $result) {
return false;
}
break;
case VoterInterface::ACCESS_DENIED:
return false;
default:
break;
if (VoterInterface::ACCESS_GRANTED === $result) {
++$grant;
}
}
}

View File

@ -175,7 +175,7 @@ abstract class AbstractAuthenticationListener extends AbstractListener
private function onFailure(Request $request, AuthenticationException $failed): Response
{
if (null !== $this->logger) {
$this->logger->error('Authentication request failed.', ['exception' => $failed]);
$this->logger->info('Authentication request failed.', ['exception' => $failed]);
}
$token = $this->tokenStorage->getToken();

View File

@ -103,8 +103,14 @@ class ObjectNormalizer extends AbstractObjectNormalizer
}
}
$checkPropertyInitialization = \PHP_VERSION_ID >= 70400;
// properties
foreach ($reflClass->getProperties(\ReflectionProperty::IS_PUBLIC) as $reflProperty) {
if ($checkPropertyInitialization && !$reflProperty->isInitialized($object)) {
continue;
}
if ($reflProperty->isStatic() || !$this->isAllowedAttribute($object, $reflProperty->name, $format, $context)) {
continue;
}

View File

@ -101,9 +101,20 @@ class PropertyNormalizer extends AbstractObjectNormalizer
{
$reflectionObject = new \ReflectionObject($object);
$attributes = [];
$checkPropertyInitialization = \PHP_VERSION_ID >= 70400;
do {
foreach ($reflectionObject->getProperties() as $property) {
if ($checkPropertyInitialization) {
if (!$property->isPublic()) {
$property->setAccessible(true);
}
if (!$property->isInitialized($object)) {
continue;
}
}
if (!$this->isAllowedAttribute($reflectionObject->getName(), $property->name, $format, $context)) {
continue;
}

View File

@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\Tests\Fixtures;
/**
* @author Valentin Udaltsov <udaltsov.valentin@gmail.com>
*/
final class Php74Dummy
{
public string $uninitializedProperty;
public string $initializedProperty = 'defaultValue';
}

View File

@ -31,6 +31,7 @@ use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Serializer\Tests\Fixtures\CircularReferenceDummy;
use Symfony\Component\Serializer\Tests\Fixtures\GroupDummy;
use Symfony\Component\Serializer\Tests\Fixtures\OtherSerializedNameDummy;
use Symfony\Component\Serializer\Tests\Fixtures\Php74Dummy;
use Symfony\Component\Serializer\Tests\Fixtures\SiblingHolder;
use Symfony\Component\Serializer\Tests\Normalizer\Features\AttributesTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\CallbacksTestTrait;
@ -111,6 +112,18 @@ class ObjectNormalizerTest extends TestCase
);
}
/**
* @requires PHP 7.4
*/
public function testNormalizeObjectWithUninitializedProperties()
{
$obj = new Php74Dummy();
$this->assertEquals(
['initializedProperty' => 'defaultValue'],
$this->normalizer->normalize($obj, 'any')
);
}
public function testDenormalize()
{
$obj = $this->normalizer->denormalize(

View File

@ -28,6 +28,7 @@ use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Serializer\Tests\Fixtures\Dummy;
use Symfony\Component\Serializer\Tests\Fixtures\GroupDummy;
use Symfony\Component\Serializer\Tests\Fixtures\GroupDummyChild;
use Symfony\Component\Serializer\Tests\Fixtures\Php74Dummy;
use Symfony\Component\Serializer\Tests\Fixtures\PropertyCircularReferenceDummy;
use Symfony\Component\Serializer\Tests\Fixtures\PropertySiblingHolder;
use Symfony\Component\Serializer\Tests\Normalizer\Features\CallbacksTestTrait;
@ -84,6 +85,18 @@ class PropertyNormalizerTest extends TestCase
);
}
/**
* @requires PHP 7.4
*/
public function testNormalizeObjectWithUninitializedProperties()
{
$obj = new Php74Dummy();
$this->assertEquals(
['initializedProperty' => 'defaultValue'],
$this->normalizer->normalize($obj, 'any')
);
}
public function testDenormalize()
{
$obj = $this->normalizer->denormalize(

View File

@ -87,16 +87,12 @@ abstract class ConstraintValidator implements ConstraintValidatorInterface
{
if (($format & self::PRETTY_DATE) && $value instanceof \DateTimeInterface) {
if (class_exists('IntlDateFormatter')) {
$locale = \Locale::getDefault();
$formatter = new \IntlDateFormatter($locale, \IntlDateFormatter::MEDIUM, \IntlDateFormatter::SHORT, $value->getTimezone());
$formatter = new \IntlDateFormatter(\Locale::getDefault(), \IntlDateFormatter::MEDIUM, \IntlDateFormatter::SHORT, 'UTC');
// neither the native nor the stub IntlDateFormatter support
// DateTimeImmutable as of yet
if (!$value instanceof \DateTime) {
$value = new \DateTime($value->format('Y-m-d H:i:s.u e'));
}
return $formatter->format($value);
return $formatter->format(new \DateTime(
$value->format('Y-m-d H:i:s.u'),
new \DateTimeZone('UTC')
));
}
return $value->format('Y-m-d H:i:s');

View File

@ -24,7 +24,7 @@ class GreaterThanOrEqualValidator extends AbstractComparisonValidator
*/
protected function compareValues($value1, $value2)
{
return $value1 >= $value2;
return null === $value2 || $value1 >= $value2;
}
/**

View File

@ -24,7 +24,7 @@ class GreaterThanValidator extends AbstractComparisonValidator
*/
protected function compareValues($value1, $value2)
{
return $value1 > $value2;
return null === $value2 || $value1 > $value2;
}
/**

View File

@ -24,7 +24,7 @@ class LessThanOrEqualValidator extends AbstractComparisonValidator
*/
protected function compareValues($value1, $value2)
{
return $value1 <= $value2;
return null === $value2 || $value1 <= $value2;
}
/**

View File

@ -24,7 +24,7 @@ class LessThanValidator extends AbstractComparisonValidator
*/
protected function compareValues($value1, $value2)
{
return $value1 < $value2;
return null === $value2 || $value1 < $value2;
}
/**

View File

@ -27,6 +27,9 @@ final class ConstraintValidatorTest extends TestCase
public function formatValueProvider()
{
$defaultTimezone = date_default_timezone_get();
date_default_timezone_set('Europe/Moscow'); // GMT+3
$data = [
['true', true],
['false', false],
@ -36,10 +39,15 @@ final class ConstraintValidatorTest extends TestCase
['array', []],
['object', $toString = new TestToStringObject()],
['ccc', $toString, ConstraintValidator::OBJECT_TO_STRING],
['object', $dateTime = (new \DateTimeImmutable('@0'))->setTimezone(new \DateTimeZone('UTC'))],
[class_exists(\IntlDateFormatter::class) ? 'Jan 1, 1970, 12:00 AM' : '1970-01-01 00:00:00', $dateTime, ConstraintValidator::PRETTY_DATE],
['object', $dateTime = new \DateTimeImmutable('1971-02-02T08:00:00UTC')],
[class_exists(\IntlDateFormatter::class) ? 'Oct 4, 2019, 11:02 AM' : '2019-10-04 11:02:03', new \DateTimeImmutable('2019-10-04T11:02:03+09:00'), ConstraintValidator::PRETTY_DATE],
[class_exists(\IntlDateFormatter::class) ? 'Feb 2, 1971, 8:00 AM' : '1971-02-02 08:00:00', $dateTime, ConstraintValidator::PRETTY_DATE],
[class_exists(\IntlDateFormatter::class) ? 'Jan 1, 1970, 6:00 AM' : '1970-01-01 06:00:00', new \DateTimeImmutable('1970-01-01T06:00:00Z'), ConstraintValidator::PRETTY_DATE],
[class_exists(\IntlDateFormatter::class) ? 'Jan 1, 1970, 3:00 PM' : '1970-01-01 15:00:00', (new \DateTimeImmutable('1970-01-01T23:00:00'))->setTimezone(new \DateTimeZone('America/New_York')), ConstraintValidator::PRETTY_DATE],
];
date_default_timezone_set($defaultTimezone);
return $data;
}
}

View File

@ -247,6 +247,32 @@ abstract class AbstractComparisonValidatorTestCase extends ConstraintValidatorTe
];
}
/**
* @dataProvider provideComparisonsToNullValueAtPropertyPath
*/
public function testCompareWithNullValueAtPropertyAt($dirtyValue, $dirtyValueAsString, $isValid)
{
$constraint = $this->createConstraint(['propertyPath' => 'value']);
$constraint->message = 'Constraint Message';
$object = new ComparisonTest_Class(null);
$this->setObject($object);
$this->validator->validate($dirtyValue, $constraint);
if ($isValid) {
$this->assertNoViolation();
} else {
$this->buildViolation('Constraint Message')
->setParameter('{{ value }}', $dirtyValueAsString)
->setParameter('{{ compared_value }}', 'null')
->setParameter('{{ compared_value_type }}', 'NULL')
->setParameter('{{ compared_value_path }}', 'value')
->setCode($this->getErrorCode())
->assertRaised();
}
}
public function provideAllInvalidComparisons(): array
{
// The provider runs before setUp(), so we need to manually fix
@ -262,6 +288,8 @@ abstract class AbstractComparisonValidatorTestCase extends ConstraintValidatorTe
abstract public function provideInvalidComparisons(): array;
abstract public function provideComparisonsToNullValueAtPropertyPath();
/**
* @param array|null $options Options for the constraint
*/

View File

@ -98,4 +98,9 @@ class DivisibleByValidatorTest extends AbstractComparisonValidatorTestCase
[\ArrayIterator::class, new \ArrayIterator(), 12],
];
}
public function provideComparisonsToNullValueAtPropertyPath()
{
$this->markTestSkipped('DivisibleByValidator rejects null values.');
}
}

View File

@ -76,4 +76,11 @@ class EqualToValidatorTest extends AbstractComparisonValidatorTestCase
[new ComparisonTest_Class(4), '4', new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'],
];
}
public function provideComparisonsToNullValueAtPropertyPath()
{
return [
[5, '5', false],
];
}
}

View File

@ -79,4 +79,11 @@ class GreaterThanOrEqualValidatorTest extends AbstractComparisonValidatorTestCas
['b', '"b"', 'c', '"c"', 'string'],
];
}
public function provideComparisonsToNullValueAtPropertyPath()
{
return [
[5, '5', true],
];
}
}

View File

@ -81,4 +81,11 @@ class GreaterThanValidatorTest extends AbstractComparisonValidatorTestCase
['22', '"22"', '22', '"22"', 'string'],
];
}
public function provideComparisonsToNullValueAtPropertyPath()
{
return [
[5, '5', true],
];
}
}

View File

@ -94,4 +94,11 @@ class IdenticalToValidatorTest extends AbstractComparisonValidatorTestCase
[new ComparisonTest_Class(4), '4', new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'],
];
}
public function provideComparisonsToNullValueAtPropertyPath()
{
return [
[5, '5', false],
];
}
}

View File

@ -82,4 +82,11 @@ class LessThanOrEqualValidatorTest extends AbstractComparisonValidatorTestCase
['c', '"c"', 'b', '"b"', 'string'],
];
}
public function provideComparisonsToNullValueAtPropertyPath()
{
return [
[5, '5', true],
];
}
}

View File

@ -80,4 +80,11 @@ class LessThanValidatorTest extends AbstractComparisonValidatorTestCase
['333', '"333"', '22', '"22"', 'string'],
];
}
public function provideComparisonsToNullValueAtPropertyPath()
{
return [
[5, '5', true],
];
}
}

View File

@ -76,4 +76,11 @@ class NotEqualToValidatorTest extends AbstractComparisonValidatorTestCase
[new ComparisonTest_Class(5), '5', new ComparisonTest_Class(5), '5', __NAMESPACE__.'\ComparisonTest_Class'],
];
}
public function provideComparisonsToNullValueAtPropertyPath()
{
return [
[5, '5', true],
];
}
}

View File

@ -94,4 +94,11 @@ class NotIdenticalToValidatorTest extends AbstractComparisonValidatorTestCase
return $comparisons;
}
public function provideComparisonsToNullValueAtPropertyPath()
{
return [
[5, '5', true],
];
}
}