From 536e53f1846394089b5f06e11572e6ff3cc2dfb0 Mon Sep 17 00:00:00 2001 From: Javier Spagnoletti Date: Tue, 4 Apr 2017 01:17:01 -0300 Subject: [PATCH] =?UTF-8?q?[Validator]=C2=A0add=20new=20`Timezone`=20valid?= =?UTF-8?q?ation=20constraint.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Symfony/Component/Validator/CHANGELOG.md | 1 + .../Validator/Constraints/Timezone.php | 51 ++++ .../Constraints/TimezoneValidator.php | 92 ++++++ .../Resources/translations/validators.de.xlf | 4 + .../Resources/translations/validators.en.xlf | 4 + .../Resources/translations/validators.es.xlf | 4 + .../Resources/translations/validators.fr.xlf | 4 + .../Tests/Constraints/TimezoneTest.php | 63 ++++ .../Constraints/TimezoneValidatorTest.php | 274 ++++++++++++++++++ 9 files changed, 497 insertions(+) create mode 100644 src/Symfony/Component/Validator/Constraints/Timezone.php create mode 100644 src/Symfony/Component/Validator/Constraints/TimezoneValidator.php create mode 100644 src/Symfony/Component/Validator/Tests/Constraints/TimezoneTest.php create mode 100644 src/Symfony/Component/Validator/Tests/Constraints/TimezoneValidatorTest.php diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index c44c49bee8c3..237dc68147b8 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -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` diff --git a/src/Symfony/Component/Validator/Constraints/Timezone.php b/src/Symfony/Component/Validator/Constraints/Timezone.php new file mode 100644 index 000000000000..b7323fb87411 --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/Timezone.php @@ -0,0 +1,51 @@ + + * + * 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 + * @author Hugo Hamon + */ +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`.'); + } + } +} diff --git a/src/Symfony/Component/Validator/Constraints/TimezoneValidator.php b/src/Symfony/Component/Validator/Constraints/TimezoneValidator.php new file mode 100644 index 000000000000..14b1668bb595 --- /dev/null +++ b/src/Symfony/Component/Validator/Constraints/TimezoneValidator.php @@ -0,0 +1,92 @@ + + * + * 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 + * @author Hugo Hamon + */ +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; + } +} diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.de.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.de.xlf index 2fc62d942e8e..64543d5e8de2 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.de.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.de.xlf @@ -354,6 +354,10 @@ This collection should contain only unique elements. Diese Sammlung darf keine doppelten Elemente enthalten. + + This value is not a valid timezone. + Dieser Wert ist keine gültige Zeitzone. + diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.en.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.en.xlf index 39c47e1275c5..d74b0fefcafa 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.en.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.en.xlf @@ -354,6 +354,10 @@ This collection should contain only unique elements. This collection should contain only unique elements. + + This value is not a valid timezone. + This value is not a valid timezone. + diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.es.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.es.xlf index 69ab34e8b29c..2fcdc44f3e4e 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.es.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.es.xlf @@ -330,6 +330,10 @@ This Business Identifier Code (BIC) is not associated with IBAN {{ iban }}. Este Código de Identificación Bancaria (BIC) no está asociado con el IBAN {{ iban }}. + + This value is not a valid timezone. + Este valor no es una zona horaria válida. + diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf index 7b1799d53315..b802c0950eb2 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf @@ -334,6 +334,10 @@ This value should be valid JSON. Cette valeur doit être un JSON valide. + + This value is not a valid timezone. + Cette valeur n'est pas un fuseau horaire valide. + diff --git a/src/Symfony/Component/Validator/Tests/Constraints/TimezoneTest.php b/src/Symfony/Component/Validator/Tests/Constraints/TimezoneTest.php new file mode 100644 index 000000000000..b7ca37486ed9 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/TimezoneTest.php @@ -0,0 +1,63 @@ + + * + * 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 + */ +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', + ]); + } +} diff --git a/src/Symfony/Component/Validator/Tests/Constraints/TimezoneValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/TimezoneValidatorTest.php new file mode 100644 index 000000000000..e18bab86fe67 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/TimezoneValidatorTest.php @@ -0,0 +1,274 @@ + + * + * 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 + * @author Hugo Hamon + */ +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']; + } +}