Merge branch '4.3' into 4.4

* 4.3:
  fix merge
  CS
  [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
  [DI] Improve performance of processDefinition
  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
  fix comparisons with null values at property paths
This commit is contained in:
Nicolas Grekas 2019-12-16 11:45:21 +01:00
commit 68681e49f2
33 changed files with 261 additions and 56 deletions

View File

@ -50,6 +50,21 @@ class ORMQueryBuilderLoader implements EntityLoaderInterface
*/ */
public function getEntitiesByIds($identifier, array $values) public function getEntitiesByIds($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; $qb = clone $this->queryBuilder;
$alias = current($qb->getRootAliases()); $alias = current($qb->getRootAliases());
$parameter = 'ORMQueryBuilderLoader_getEntitiesByIds_'.$identifier; $parameter = 'ORMQueryBuilderLoader_getEntitiesByIds_'.$identifier;

View File

@ -953,6 +953,31 @@ class EntityTypeTest extends BaseTypeTest
$this->assertNull($field->getData()); $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() public function testDisallowChoicesThatAreNotIncludedQueryBuilderSingleAssocIdentifier()
{ {
$innerEntity1 = new SingleIntIdNoToStringEntity(1, 'InFoo'); $innerEntity1 = new SingleIntIdNoToStringEntity(1, 'InFoo');

View File

@ -861,6 +861,11 @@ class Configuration implements ConfigurationInterface
->end() ->end()
->end() ->end()
->arrayNode('auto_mapping') ->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') ->useAttributeAsKey('namespace')
->normalizeKeys(false) ->normalizeKeys(false)
->beforeNormalization() ->beforeNormalization()

View File

@ -48,7 +48,7 @@ class TemplateNameParser extends BaseTemplateNameParser
} }
// normalize name // normalize name
$name = str_replace(':/', ':', preg_replace('#/{2,}#', '/', str_replace('\\', '/', $name))); $name = preg_replace('#/{2,}#', '/', str_replace('\\', '/', $name));
if (false !== strpos($name, '..')) { if (false !== strpos($name, '..')) {
throw new \RuntimeException(sprintf('Template name "%s" contains invalid characters.', $name)); throw new \RuntimeException(sprintf('Template name "%s" contains invalid characters.', $name));

View File

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

View File

@ -11,7 +11,9 @@
namespace Symfony\Component\Cache\Simple; namespace Symfony\Component\Cache\Simple;
use Symfony\Component\Cache\Adapter\ApcuAdapter;
use Symfony\Component\Cache\Traits\ApcuTrait; use Symfony\Component\Cache\Traits\ApcuTrait;
use Symfony\Contracts\Cache\CacheInterface;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" and type-hint for "%s" instead.', ApcuCache::class, ApcuAdapter::class, CacheInterface::class), E_USER_DEPRECATED); @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3, use "%s" and type-hint for "%s" instead.', ApcuCache::class, ApcuAdapter::class, CacheInterface::class), E_USER_DEPRECATED);

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(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('/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->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

@ -63,9 +63,10 @@ class ResolveInstanceofConditionalsPass implements CompilerPassInterface
$instanceofTags = []; $instanceofTags = [];
$instanceofCalls = []; $instanceofCalls = [];
$instanceofBindings = []; $instanceofBindings = [];
$reflectionClass = null;
foreach ($conditionals as $interface => $instanceofDefs) { 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; continue;
} }

View File

@ -28,7 +28,7 @@ interface MessageSubscriberInterface extends MessageHandlerInterface
* It can also change the priority per classes. * It can also change the priority per classes.
* *
* yield FirstMessage::class => ['priority' => 0]; * 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: * 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('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('declareQueue');
$amqpQueue->expects($this->once())->method('bind')->with(self::DEFAULT_EXCHANGE_NAME, null); $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)); $factory->method('createQueue')->will($this->onConsecutiveCalls($amqpQueue0, $amqpQueue1));
$amqpExchange->expects($this->once())->method('declareExchange'); $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->once())->method('declareQueue');
$amqpQueue0->expects($this->exactly(2))->method('bind')->withConsecutive( $amqpQueue0->expects($this->exactly(2))->method('bind')->withConsecutive(
[self::DEFAULT_EXCHANGE_NAME, 'binding_key0'], [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('declareQueue');
$delayQueue->expects($this->once())->method('bind')->with('delays', 'delay_messages__5000'); $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 = Connection::fromDsn('amqp://localhost', [], $factory);
$connection->publish('{}', ['x-some-headers' => 'foo'], 5000); $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('declareQueue');
$delayQueue->expects($this->once())->method('bind')->with('delays', 'delay_messages__120000'); $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); $connection->publish('{}', [], 120000);
} }
@ -474,12 +474,27 @@ class ConnectionTest extends TestCase
$amqpExchange = $this->createMock(\AMQPExchange::class) $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 = Connection::fromDsn('amqp://localhost', [], $factory);
$connection->publish('body', ['Foo' => 'X'], 0, new AmqpStamp(null, AMQP_NOPARAM, ['headers' => ['Bar' => 'Y']])); $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() public function testItCanPublishWithTheDefaultRoutingKey()
{ {
$factory = new TestAmqpFactory( $factory = new TestAmqpFactory(
@ -546,7 +561,7 @@ class ConnectionTest extends TestCase
$delayQueue->expects($this->once())->method('declareQueue'); $delayQueue->expects($this->once())->method('declareQueue');
$delayQueue->expects($this->once())->method('bind')->with('delays', 'delay_messages_routing_key_120000'); $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')); $connection->publish('{}', [], 120000, new AmqpStamp('routing_key'));
} }

View File

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

View File

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

View File

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

View File

@ -101,9 +101,20 @@ class PropertyNormalizer extends AbstractObjectNormalizer
{ {
$reflectionObject = new \ReflectionObject($object); $reflectionObject = new \ReflectionObject($object);
$attributes = []; $attributes = [];
$checkPropertyInitialization = \PHP_VERSION_ID >= 70400;
do { do {
foreach ($reflectionObject->getProperties() as $property) { 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)) { if (!$this->isAllowedAttribute($reflectionObject->getName(), $property->name, $format, $context)) {
continue; 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

@ -33,6 +33,7 @@ use Symfony\Component\Serializer\Tests\Fixtures\CircularReferenceDummy;
use Symfony\Component\Serializer\Tests\Fixtures\GroupDummy; use Symfony\Component\Serializer\Tests\Fixtures\GroupDummy;
use Symfony\Component\Serializer\Tests\Fixtures\MaxDepthDummy; use Symfony\Component\Serializer\Tests\Fixtures\MaxDepthDummy;
use Symfony\Component\Serializer\Tests\Fixtures\OtherSerializedNameDummy; 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\Fixtures\SiblingHolder;
use Symfony\Component\Serializer\Tests\Normalizer\Features\AttributesTestTrait; use Symfony\Component\Serializer\Tests\Normalizer\Features\AttributesTestTrait;
use Symfony\Component\Serializer\Tests\Normalizer\Features\CallbacksObject; use Symfony\Component\Serializer\Tests\Normalizer\Features\CallbacksObject;
@ -114,6 +115,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() public function testDenormalize()
{ {
$obj = $this->normalizer->denormalize( $obj = $this->normalizer->denormalize(

View File

@ -30,6 +30,7 @@ use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Serializer\Tests\Fixtures\Dummy; use Symfony\Component\Serializer\Tests\Fixtures\Dummy;
use Symfony\Component\Serializer\Tests\Fixtures\GroupDummy; use Symfony\Component\Serializer\Tests\Fixtures\GroupDummy;
use Symfony\Component\Serializer\Tests\Fixtures\GroupDummyChild; 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\PropertyCircularReferenceDummy;
use Symfony\Component\Serializer\Tests\Fixtures\PropertySiblingHolder; use Symfony\Component\Serializer\Tests\Fixtures\PropertySiblingHolder;
use Symfony\Component\Serializer\Tests\Normalizer\Features\CallbacksObject; use Symfony\Component\Serializer\Tests\Normalizer\Features\CallbacksObject;
@ -87,6 +88,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() public function testDenormalize()
{ {
$obj = $this->normalizer->denormalize( $obj = $this->normalizer->denormalize(

View File

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

View File

@ -24,7 +24,7 @@ class GreaterThanOrEqualValidator extends AbstractComparisonValidator
*/ */
protected function compareValues($value1, $value2) 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) 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) 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) 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() public function formatValueProvider()
{ {
$defaultTimezone = date_default_timezone_get();
date_default_timezone_set('Europe/Moscow'); // GMT+3
$data = [ $data = [
['true', true], ['true', true],
['false', false], ['false', false],
@ -36,10 +39,15 @@ final class ConstraintValidatorTest extends TestCase
['array', []], ['array', []],
['object', $toString = new TestToStringObject()], ['object', $toString = new TestToStringObject()],
['ccc', $toString, ConstraintValidator::OBJECT_TO_STRING], ['ccc', $toString, ConstraintValidator::OBJECT_TO_STRING],
['object', $dateTime = (new \DateTimeImmutable('@0'))->setTimezone(new \DateTimeZone('UTC'))], ['object', $dateTime = new \DateTimeImmutable('1971-02-02T08:00:00UTC')],
[class_exists(\IntlDateFormatter::class) ? 'Jan 1, 1970, 12:00 AM' : '1970-01-01 00:00:00', $dateTime, ConstraintValidator::PRETTY_DATE], [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; 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 public function provideAllInvalidComparisons(): array
{ {
// The provider runs before setUp(), so we need to manually fix // 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 provideInvalidComparisons(): array;
abstract public function provideComparisonsToNullValueAtPropertyPath();
/** /**
* @param array|null $options Options for the constraint * @param array|null $options Options for the constraint
*/ */

View File

@ -98,4 +98,9 @@ class DivisibleByValidatorTest extends AbstractComparisonValidatorTestCase
[\ArrayIterator::class, new \ArrayIterator(), 12], [\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'], [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'], ['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'], ['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'], [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'], ['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'], ['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'], [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; return $comparisons;
} }
public function provideComparisonsToNullValueAtPropertyPath()
{
return [
[5, '5', true],
];
}
} }