[Validator] add new Timezone validation constraint.

This commit is contained in:
Javier Spagnoletti 2017-04-04 01:17:01 -03:00 committed by Hugo Hamon
parent a68b4c7191
commit 536e53f184
9 changed files with 497 additions and 0 deletions

View File

@ -4,6 +4,7 @@ CHANGELOG
4.3.0
-----
* added `Timezone` constraint
* added `NotCompromisedPassword` constraint
* added options `iban` and `ibanPropertyPath` to Bic constraint
* added UATP cards support to `CardSchemeValidator`

View File

@ -0,0 +1,51 @@
<?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\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
/**
* @Annotation
* @Target({"PROPERTY", "METHOD", "ANNOTATION"})
*
* @author Javier Spagnoletti <phansys@gmail.com>
* @author Hugo Hamon <hugohamon@neuf.fr>
*/
class Timezone extends Constraint
{
public const TIMEZONE_IDENTIFIER_ERROR = '5ce113e6-5e64-4ea2-90fe-d2233956db13';
public const TIMEZONE_IDENTIFIER_IN_ZONE_ERROR = 'b57767b1-36c0-40ac-a3d7-629420c775b8';
public const TIMEZONE_IDENTIFIER_IN_COUNTRY_ERROR = 'c4a22222-dc92-4fc0-abb0-d95b268c7d0b';
public $zone = \DateTimeZone::ALL;
public $countryCode;
public $message = 'This value is not a valid timezone.';
protected static $errorNames = [
self::TIMEZONE_IDENTIFIER_ERROR => 'TIMEZONE_IDENTIFIER_ERROR',
self::TIMEZONE_IDENTIFIER_IN_ZONE_ERROR => 'TIMEZONE_IDENTIFIER_IN_ZONE_ERROR',
self::TIMEZONE_IDENTIFIER_IN_COUNTRY_ERROR => 'TIMEZONE_IDENTIFIER_IN_COUNTRY_ERROR',
];
/**
* {@inheritdoc}
*/
public function __construct(array $options = null)
{
parent::__construct($options);
if ($this->countryCode && \DateTimeZone::PER_COUNTRY !== $this->zone) {
throw new ConstraintDefinitionException('The option "countryCode" can only be used when "zone" option is configured with `\DateTimeZone::PER_COUNTRY`.');
}
}
}

View File

@ -0,0 +1,92 @@
<?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\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
/**
* Validates whether a value is a valid timezone identifier.
*
* @author Javier Spagnoletti <phansys@gmail.com>
* @author Hugo Hamon <hugohamon@neuf.fr>
*/
class TimezoneValidator extends ConstraintValidator
{
/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof Timezone) {
throw new UnexpectedTypeException($constraint, Timezone::class);
}
if (null === $value || '' === $value) {
return;
}
if (!is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) {
throw new UnexpectedValueException($value, 'string');
}
$value = (string) $value;
// @see: https://bugs.php.net/bug.php?id=75928
if ($constraint->countryCode) {
$timezoneIds = \DateTimeZone::listIdentifiers($constraint->zone, $constraint->countryCode);
} else {
$timezoneIds = \DateTimeZone::listIdentifiers($constraint->zone);
}
if ($timezoneIds && \in_array($value, $timezoneIds, true)) {
return;
}
if ($constraint->countryCode) {
$code = Timezone::TIMEZONE_IDENTIFIER_IN_COUNTRY_ERROR;
} elseif (\DateTimeZone::ALL !== $constraint->zone) {
$code = Timezone::TIMEZONE_IDENTIFIER_IN_ZONE_ERROR;
} else {
$code = Timezone::TIMEZONE_IDENTIFIER_ERROR;
}
$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $this->formatValue($value))
->setCode($code)
->addViolation();
}
/**
* {@inheritdoc}
*/
public function getDefaultOption()
{
return 'zone';
}
/**
* {@inheritdoc}
*/
protected function formatValue($value, $format = 0)
{
$value = parent::formatValue($value, $format);
if (!$value || \DateTimeZone::PER_COUNTRY === $value) {
return $value;
}
return array_search($value, (new \ReflectionClass(\DateTimeZone::class))->getConstants(), true) ?: $value;
}
}

View File

@ -354,6 +354,10 @@
<source>This collection should contain only unique elements.</source>
<target>Diese Sammlung darf keine doppelten Elemente enthalten.</target>
</trans-unit>
<trans-unit id="92">
<source>This value is not a valid timezone.</source>
<target>Dieser Wert ist keine gültige Zeitzone.</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -354,6 +354,10 @@
<source>This collection should contain only unique elements.</source>
<target>This collection should contain only unique elements.</target>
</trans-unit>
<trans-unit id="92">
<source>This value is not a valid timezone.</source>
<target>This value is not a valid timezone.</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -330,6 +330,10 @@
<source>This Business Identifier Code (BIC) is not associated with IBAN {{ iban }}.</source>
<target>Este Código de Identificación Bancaria (BIC) no está asociado con el IBAN {{ iban }}.</target>
</trans-unit>
<trans-unit id="92">
<source>This value is not a valid timezone.</source>
<target>Este valor no es una zona horaria válida.</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -334,6 +334,10 @@
<source>This value should be valid JSON.</source>
<target>Cette valeur doit être un JSON valide.</target>
</trans-unit>
<trans-unit id="92">
<source>This value is not a valid timezone.</source>
<target>Cette valeur n'est pas un fuseau horaire valide.</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@ -0,0 +1,63 @@
<?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\Validator\Tests\Constraints;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Constraints\Timezone;
/**
* @author Javier Spagnoletti <phansys@gmail.com>
*/
class TimezoneTest extends TestCase
{
public function testValidTimezoneConstraints()
{
$constraint = new Timezone();
$constraint = new Timezone([
'message' => 'myMessage',
'zone' => \DateTimeZone::PER_COUNTRY,
'countryCode' => 'AR',
]);
$constraint = new Timezone([
'message' => 'myMessage',
'zone' => \DateTimeZone::ALL,
]);
// Make an assertion in order to avoid this test to be marked as risky
$this->assertInstanceOf(Timezone::class, $constraint);
}
/**
* @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException
*/
public function testExceptionForGroupedTimezonesByCountryWithWrongTimezone()
{
$constraint = new Timezone([
'message' => 'myMessage',
'zone' => \DateTimeZone::ALL,
'countryCode' => 'AR',
]);
}
/**
* @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException
*/
public function testExceptionForGroupedTimezonesByCountryWithoutTimezone()
{
$constraint = new Timezone([
'message' => 'myMessage',
'countryCode' => 'AR',
]);
}
}

View File

@ -0,0 +1,274 @@
<?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\Validator\Tests\Constraints;
use Symfony\Component\Validator\Constraints\Timezone;
use Symfony\Component\Validator\Constraints\TimezoneValidator;
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
/**
* @author Javier Spagnoletti <phansys@gmail.com>
* @author Hugo Hamon <hugohamon@neuf.fr>
*/
class TimezoneValidatorTest extends ConstraintValidatorTestCase
{
protected function createValidator(): TimezoneValidator
{
return new TimezoneValidator();
}
public function testNullIsValid()
{
$this->validator->validate(null, new Timezone());
$this->assertNoViolation();
}
public function testEmptyStringIsValid()
{
$this->validator->validate('', new Timezone());
$this->assertNoViolation();
}
/**
* @expectedException \Symfony\Component\Validator\Exception\UnexpectedValueException
*/
public function testExpectsStringCompatibleType()
{
$this->validator->validate(new \stdClass(), new Timezone());
}
/**
* @dataProvider getValidTimezones
*/
public function testValidTimezones(string $timezone)
{
$this->validator->validate($timezone, new Timezone());
$this->assertNoViolation();
}
public function getValidTimezones(): iterable
{
yield ['America/Argentina/Buenos_Aires'];
yield ['America/Barbados'];
yield ['America/Toronto'];
yield ['Antarctica/Syowa'];
yield ['Africa/Douala'];
yield ['Atlantic/Canary'];
yield ['Asia/Gaza'];
yield ['Australia/Sydney'];
yield ['Europe/Copenhagen'];
yield ['Europe/Paris'];
yield ['Pacific/Noumea'];
yield ['UTC'];
}
/**
* @dataProvider getValidGroupedTimezones
*/
public function testValidGroupedTimezones(string $timezone, int $zone)
{
$constraint = new Timezone([
'zone' => $zone,
]);
$this->validator->validate($timezone, $constraint);
$this->assertNoViolation();
}
public function getValidGroupedTimezones(): iterable
{
yield ['America/Argentina/Cordoba', \DateTimeZone::AMERICA];
yield ['America/Barbados', \DateTimeZone::AMERICA];
yield ['Africa/Cairo', \DateTimeZone::AFRICA];
yield ['Atlantic/Cape_Verde', \DateTimeZone::ATLANTIC];
yield ['Europe/Bratislava', \DateTimeZone::EUROPE];
yield ['Indian/Christmas', \DateTimeZone::INDIAN];
yield ['Pacific/Kiritimati', \DateTimeZone::ALL];
yield ['Pacific/Kiritimati', \DateTimeZone::ALL_WITH_BC];
yield ['Pacific/Kiritimati', \DateTimeZone::PACIFIC];
yield ['Arctic/Longyearbyen', \DateTimeZone::ARCTIC];
yield ['Asia/Beirut', \DateTimeZone::ASIA];
yield ['Atlantic/Bermuda', \DateTimeZone::ASIA | \DateTimeZone::ATLANTIC];
yield ['Atlantic/Azores', \DateTimeZone::ATLANTIC | \DateTimeZone::ASIA];
}
/**
* @dataProvider getInvalidTimezones
*/
public function testInvalidTimezoneWithoutZone(string $timezone)
{
$constraint = new Timezone([
'message' => 'myMessage',
]);
$this->validator->validate($timezone, $constraint);
$this->buildViolation('myMessage')
->setParameter('{{ value }}', sprintf('"%s"', $timezone))
->setCode(Timezone::TIMEZONE_IDENTIFIER_ERROR)
->assertRaised();
}
public function getInvalidTimezones(): iterable
{
yield ['Buenos_Aires/Argentina/America'];
yield ['Mayotte/Indian'];
yield ['foobar'];
}
/**
* @dataProvider getInvalidGroupedTimezones
*/
public function testInvalidGroupedTimezones(string $timezone, int $zone)
{
$constraint = new Timezone([
'zone' => $zone,
'message' => 'myMessage',
]);
$this->validator->validate($timezone, $constraint);
$this->buildViolation('myMessage')
->setParameter('{{ value }}', sprintf('"%s"', $timezone))
->setCode(Timezone::TIMEZONE_IDENTIFIER_IN_ZONE_ERROR)
->assertRaised();
}
public function getInvalidGroupedTimezones(): iterable
{
yield ['Antarctica/McMurdo', \DateTimeZone::AMERICA];
yield ['America/Barbados', \DateTimeZone::ANTARCTICA];
yield ['Europe/Kiev', \DateTimeZone::ARCTIC];
yield ['Asia/Ho_Chi_Minh', \DateTimeZone::INDIAN];
yield ['Asia/Ho_Chi_Minh', \DateTimeZone::INDIAN | \DateTimeZone::ANTARCTICA];
}
/**
* @dataProvider getValidGroupedTimezonesByCountry
*/
public function testValidGroupedTimezonesByCountry(string $timezone, string $country)
{
$constraint = new Timezone([
'zone' => \DateTimeZone::PER_COUNTRY,
'countryCode' => $country,
]);
$this->validator->validate($timezone, $constraint);
$this->assertNoViolation();
}
public function getValidGroupedTimezonesByCountry(): iterable
{
yield ['America/Argentina/Cordoba', 'AR'];
yield ['America/Barbados', 'BB'];
yield ['Africa/Cairo', 'EG'];
yield ['Arctic/Longyearbyen', 'SJ'];
yield ['Asia/Beirut', 'LB'];
yield ['Atlantic/Azores', 'PT'];
yield ['Atlantic/Bermuda', 'BM'];
yield ['Atlantic/Cape_Verde', 'CV'];
yield ['Australia/Sydney', 'AU'];
yield ['Australia/Melbourne', 'AU'];
yield ['Europe/Bratislava', 'SK'];
yield ['Europe/Paris', 'FR'];
yield ['Europe/Madrid', 'ES'];
yield ['Europe/Monaco', 'MC'];
yield ['Indian/Christmas', 'CX'];
yield ['Pacific/Kiritimati', 'KI'];
yield ['Pacific/Kiritimati', 'KI'];
yield ['Pacific/Kiritimati', 'KI'];
}
/**
* @dataProvider getInvalidGroupedTimezonesByCountry
*/
public function testInvalidGroupedTimezonesByCountry(string $timezone, string $invalidCountryCode)
{
$constraint = new Timezone([
'message' => 'myMessage',
'zone' => \DateTimeZone::PER_COUNTRY,
'countryCode' => $invalidCountryCode,
]);
$this->validator->validate($timezone, $constraint);
$this->buildViolation('myMessage')
->setParameter('{{ value }}', sprintf('"%s"', $timezone))
->setCode(Timezone::TIMEZONE_IDENTIFIER_IN_COUNTRY_ERROR)
->assertRaised();
}
public function getInvalidGroupedTimezonesByCountry(): iterable
{
yield ['America/Argentina/Cordoba', 'FR'];
yield ['America/Barbados', 'PT'];
yield ['Europe/Bern', 'FR'];
}
/**
* @dataProvider getDeprecatedTimezones
*/
public function testDeprecatedTimezonesAreValidWithBC(string $timezone)
{
$constraint = new Timezone([
'zone' => \DateTimeZone::ALL_WITH_BC,
]);
$this->validator->validate($timezone, $constraint);
$this->assertNoViolation();
}
/**
* @dataProvider getDeprecatedTimezones
*/
public function testDeprecatedTimezonesAreInvalidWithoutBC(string $timezone)
{
$constraint = new Timezone([
'message' => 'myMessage',
]);
$this->validator->validate($timezone, $constraint);
$this->buildViolation('myMessage')
->setParameter('{{ value }}', sprintf('"%s"', $timezone))
->setCode(Timezone::TIMEZONE_IDENTIFIER_ERROR)
->assertRaised();
}
public function getDeprecatedTimezones(): iterable
{
yield ['America/Buenos_Aires'];
yield ['America/Montreal'];
yield ['Australia/ACT'];
yield ['Australia/LHI'];
yield ['Australia/Queensland'];
yield ['Canada/Eastern'];
yield ['Canada/Central'];
yield ['Canada/Mountain'];
yield ['Canada/Pacific'];
yield ['CET'];
yield ['CST6CDT'];
yield ['Etc/GMT'];
yield ['Etc/Greenwich'];
yield ['Etc/UCT'];
yield ['Etc/Universal'];
yield ['Etc/UTC'];
yield ['Etc/Zulu'];
yield ['US/Pacific'];
}
}