Apply contribution guidelines to "DateTime" rule

This commit also makes some changes in how the `DateTime` rule behaves,
by not accepting `DateTimeInterface` as valid when a format is given.

Also:
- Create `DateTimeHelper` to eliminate some code duplication;
- Create integration tests for `DateTime` rule;
- Rename "format" placeholder to "sample" in the message;
- Update documentation of "DateTime" rule.
This commit is contained in:
Henrique Moody 2018-01-30 18:57:07 +01:00
parent 2007c7dc6e
commit fb2ebaf5d6
No known key found for this signature in database
GPG key ID: 221E9281655813A6
9 changed files with 351 additions and 177 deletions

View file

@ -3,8 +3,8 @@
- `DateTime()`
- `DateTime(string $format)`
Validates if input is a date. The `$format` argument should be in accordance to
PHP's [date()](http://php.net/date) function.
Validates whether an input is a date/time or not. The `$format` argument should
be in accordance to PHP's [date()](http://php.net/date) function.
```php
v::dateTime()->validate('2009-01-01'); // true
@ -43,7 +43,9 @@ Version | Description
***
See also:
- [Date](Date.md)
- [Between](Between.md)
- [MinimumAge](MinimumAge.md)
- [LeapDate](LeapDate.md)
- [LeapYear](LeapYear.md)
- [Time](Time.md)

View file

@ -13,24 +13,36 @@ declare(strict_types=1);
namespace Respect\Validation\Exceptions;
class DateTimeException extends ValidationException
{
const FORMAT = 1;
use function strtotime;
/**
* @author Alexandre Gomes Gaigalas <alexandre@gaigalas.net>
* @author Henrique Moody <henriquemoody@gmail.com>
*/
final class DateTimeException extends ValidationException
{
public const FORMAT = 1;
/**
* {@inheritdoc}
*/
public static $defaultTemplates = [
self::MODE_DEFAULT => [
self::STANDARD => '{{name}} must be a valid date/time',
self::FORMAT => '{{name}} must be a valid date/time. Sample format: {{format}}',
self::FORMAT => '{{name}} must be a valid date/time in the format {{sample}}',
],
self::MODE_NEGATIVE => [
self::STANDARD => '{{name}} must not be a valid date/time',
self::FORMAT => '{{name}} must not be a valid date/time in the format {{format}}',
self::FORMAT => '{{name}} must not be a valid date/time in the format {{sample}}',
],
];
/**
* {@inheritdoc}
*/
public function configure($name, array $params = [])
{
$params['format'] = date(
$params['sample'] = date(
(string) $params['format'],
strtotime('2005-12-30 01:02:03')
);
@ -38,6 +50,9 @@ class DateTimeException extends ValidationException
return parent::configure($name, $params);
}
/**
* {@inheritdoc}
*/
public function chooseTemplate()
{
return $this->getParam('format') ? static::FORMAT : static::STANDARD;

View file

@ -0,0 +1,42 @@
<?php
/*
* This file is part of Respect/Validation.
*
* (c) Alexandre Gomes Gaigalas <alexandre@gaigalas.net>
*
* For the full copyright and license information, please view the "LICENSE.md"
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Respect\Validation\Helpers;
/**
* Helper to handle date/time.
*
* @author Henrique Moody <henriquemoody@gmail.com>
*/
trait DateTimeHelper
{
/**
* Finds whether a value is a valid date/time in a specific format.
*
* @param string $format
* @param string $value
*
* @return bool
*/
private function isDateTime(string $format, string $value): bool
{
$exceptionalFormats = [
'c' => 'Y-m-d\TH:i:sP',
'r' => 'D, d M Y H:i:s O',
];
$info = date_parse_from_format($exceptionalFormats[$format] ?? $format, $value);
return ($info['error_count'] + $info['warning_count']) === 0;
}
}

View file

@ -17,6 +17,7 @@ use function date_parse_from_format;
use function is_scalar;
use function preg_match;
use function sprintf;
use Respect\Validation\Helpers\DateTimeHelper;
use Respect\Validation\Exceptions\ComponentException;
/**
@ -27,6 +28,8 @@ use Respect\Validation\Exceptions\ComponentException;
*/
final class Date extends AbstractRule
{
use DateTimeHelper;
/**
* @var string
*/
@ -57,8 +60,6 @@ final class Date extends AbstractRule
return false;
}
$info = date_parse_from_format($this->format, (string) $input);
return 0 === $info['error_count'] && 0 === $info['warning_count'];
return $this->isDateTime($this->format, (string) $input);
}
}

View file

@ -13,44 +13,52 @@ declare(strict_types=1);
namespace Respect\Validation\Rules;
use function is_scalar;
use function strtotime;
use DateTimeInterface;
use Respect\Validation\Helpers\DateTimeHelper;
class DateTime extends AbstractRule
/**
* @author Alexandre Gomes Gaigalas <alexandre@gaigalas.net>
* @author Emmerson Siqueira <emmersonsiqueira@gmail.com>
* @author Henrique Moody <henriquemoody@gmail.com>
*/
final class DateTime extends AbstractRule
{
public $format = null;
use DateTimeHelper;
public function __construct($format = null)
/**
* @var string|null
*/
private $format;
/**
* Initializes the rule.
*
* @param string|null $format
*/
public function __construct(string $format = null)
{
$this->format = $format;
}
/**
* {@inheritdoc}
*/
public function validate($input): bool
{
if ($input instanceof DateTimeInterface) {
return true;
return null === $this->format;
}
if (!is_scalar($input)) {
return false;
}
$inputString = (string) $input;
if (is_null($this->format)) {
return false !== strtotime($inputString);
if (null === $this->format) {
return false !== strtotime((string) $input);
}
$exceptionalFormats = [
'c' => 'Y-m-d\TH:i:sP',
'r' => 'D, d M Y H:i:s O',
];
if (in_array($this->format, array_keys($exceptionalFormats))) {
$this->format = $exceptionalFormats[$this->format];
}
$info = date_parse_from_format($this->format, $inputString);
return 0 === $info['error_count'] && 0 === $info['warning_count'];
return $this->isDateTime($this->format, (string) $input);
}
}

View file

@ -17,6 +17,7 @@ use function date_parse_from_format;
use function is_scalar;
use function preg_match;
use function sprintf;
use Respect\Validation\Helpers\DateTimeHelper;
use Respect\Validation\Exceptions\ComponentException;
/**
@ -26,6 +27,8 @@ use Respect\Validation\Exceptions\ComponentException;
*/
final class Time extends AbstractRule
{
use DateTimeHelper;
/**
* @var string
*/
@ -56,8 +59,6 @@ final class Time extends AbstractRule
return false;
}
$info = date_parse_from_format($this->format, (string) $input);
return ($info['error_count'] + $info['warning_count']) === 0;
return $this->isDateTime($this->format, (string) $input);
}
}

View file

@ -0,0 +1,66 @@
--FILE--
<?php
require 'vendor/autoload.php';
use Respect\Validation\Exceptions\DateTimeException;
use Respect\Validation\Exceptions\NestedValidationException;
use Respect\Validation\Validator as v;
try {
v::dateTime()->check('FooBarBazz');
} catch (DateTimeException $exception) {
echo $exception->getMessage().PHP_EOL;
}
try {
v::dateTime('c')->check('06-12-1995');
} catch (DateTimeException $exception) {
echo $exception->getMessage().PHP_EOL;
}
try {
v::dateTime()->assert('QuxQuuxx');
} catch (NestedValidationException $exception) {
echo $exception->getFullMessage().PHP_EOL;
}
try {
v::dateTime('r')->assert(2018013030);
} catch (NestedValidationException $exception) {
echo $exception->getFullMessage().PHP_EOL;
}
try {
v::not(v::dateTime())->check('4 days ago');
} catch (DateTimeException $exception) {
echo $exception->getMessage().PHP_EOL;
}
try {
v::not(v::dateTime('Y-m-d'))->check('1988-09-09');
} catch (DateTimeException $exception) {
echo $exception->getMessage().PHP_EOL;
}
try {
v::not(v::dateTime())->assert('+3 weeks');
} catch (NestedValidationException $exception) {
echo $exception->getFullMessage().PHP_EOL;
}
try {
v::not(v::dateTime('d/m/y'))->assert('23/07/99');
} catch (NestedValidationException $exception) {
echo $exception->getFullMessage().PHP_EOL;
}
?>
--EXPECTF--
"FooBarBazz" must be a valid date/time
"06-12-1995" must be a valid date/time in the format "2005-12-30T01:02:03+00:00"
- "QuxQuuxx" must be a valid date/time
- 2018013030 must be a valid date/time in the format "Fri, 30 Dec 2005 01:02:03 +0000"
"4 days ago" must not be a valid date/time
"1988-09-09" must not be a valid date/time in the format "2005-12-30"
- "+3 weeks" must not be a valid date/time
- "23/07/99" must not be a valid date/time in the format "30/12/05"

View file

@ -0,0 +1,81 @@
<?php
/*
* This file is part of Respect/Validation.
*
* (c) Alexandre Gomes Gaigalas <alexandre@gaigalas.net>
*
* For the full copyright and license information, please view the "LICENSE.md"
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Respect\Validation\Test\Helpers;
use PHPUnit\Framework\TestCase;
use Respect\Validation\Helpers\DateTimeHelper;
/**
* @group helper
*
* @covers \Respect\Validation\Helpers\DateTimeHelper
*
* @author Henrique Moody <henriquemoody@gmail.com>
*/
final class DateTimeHelperTest extends TestCase
{
use DateTimeHelper;
public function providerForValidDateTime(): array
{
return [
['Y-m-d', '0000-01-01'],
['Y-m-d', '2009-09-09'],
['Y-m-d', '2020-02-29'],
['Ymd', '20090909'],
['d/m/Y', '23/05/1987'],
['c', '2018-01-30T19:04:35+00:00'],
['Y-m-d\TH:i:sP', '2018-01-30T19:04:35+00:00'],
['r', 'Tue, 30 Jan 2018 19:06:01 +0000'],
['D, d M Y H:i:s O', 'Tue, 30 Jan 2018 19:06:01 +0000'],
];
}
/**
* @test
*
* @dataProvider providerForValidDateTime
*
* @param string $format
* @param string $value
*/
public function shouldFindWhenValueIsDateTime(string $format, string $value): void
{
self::assertTrue($this->isDateTime($format, $value));
}
public function providerForInvalidDateTime(): array
{
return [
['Y-m-d', '2021-02-29'],
['y-m-d', '2009-09-12'],
['Y-m-d', '0000-00-31'],
['Y-m-d', '0000-12-00'],
['Y-m-d H:i:s', '1987-12-31'],
];
}
/**
* @test
*
* @dataProvider providerForInvalidDateTime
*
* @param string $format
* @param string $value
*/
public function shouldFindWhenValueIsNotDateTime(string $format, string $value): void
{
self::assertFalse($this->isDateTime($format, $value));
}
}

View file

@ -13,159 +13,117 @@ declare(strict_types=1);
namespace Respect\Validation\Rules;
use function date_default_timezone_get;
use function date_default_timezone_set;
use DateTime as DateTimeMutable;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
use Respect\Validation\Test\RuleTestCase;
/**
* @group rule
* @group rule
*
* @covers \Respect\Validation\Rules\DateTime
* @covers \Respect\Validation\Exceptions\DateTimeException
*
* @author Alexandre Gomes Gaigalas <alexandre@gaigalas.net>
* @author Henrique Moody <henriquemoody@gmail.com>
*/
class DateTimeTest extends TestCase
final class DateTimeTest extends RuleTestCase
{
protected $dateValidator;
protected function setUp(): void
{
$this->dateValidator = new DateTime();
}
public function testDateEmptyShouldNotValidate(): void
{
self::assertFalse($this->dateValidator->__invoke(''));
}
/**
* @expectedException \Respect\Validation\Exceptions\DateTimeException
* {@inheritdoc}
*/
public function testDateEmptyShouldNotCheck(): void
{
$this->dateValidator->check('');
}
/**
* @doesNotPerformAssertions
*
* @expectedException \Respect\Validation\Exceptions\DateTimeException
*/
public function testDateEmptyShouldNotAssert(): void
{
$this->dateValidator->assert('');
}
public function testDateWithoutFormatShouldValidate(): void
{
self::assertTrue($this->dateValidator->__invoke('today'));
}
public function testDateTimeInstancesShouldAlwaysValidate(): void
{
self::assertTrue($this->dateValidator->__invoke(new \DateTime('today')));
}
public function testDateTimeImmutableInterfaceInstancesShouldAlwaysValidate()
{
if (!class_exists('DateTimeImmutable')) {
return $this->markTestSkipped('DateTimeImmutable does not exist');
}
self::assertTrue($this->dateValidator->validate(new DateTimeImmutable('today')));
}
public function testInvalidDateShouldFail(): void
{
self::assertFalse($this->dateValidator->__invoke('aids'));
}
public function testInvalidDateShouldFail_on_invalid_conversions(): void
{
$this->dateValidator->format = 'Y-m-d';
self::assertFalse($this->dateValidator->__invoke('2009-12-00'));
}
public function testAnyObjectExceptDateTimeInstancesShouldFail(): void
{
self::assertFalse($this->dateValidator->__invoke(new \stdClass()));
}
/**
* @doesNotPerformAssertions
*/
public function testFormatsShouldValidateDateStrings(): void
{
$this->dateValidator = new DateTime('Y-m-d');
$this->dateValidator->assert('2009-09-09');
}
/**
* @doesNotPerformAssertions
*/
public function testFormatsShouldValidateDateStrings_with_any_formats(): void
{
$this->dateValidator = new DateTime('d/m/Y');
$this->dateValidator->assert('23/05/1987');
}
/**
* @expectedException \Respect\Validation\Exceptions\DateTimeException
*/
public function testFormatsShouldValidateDateStrings_and_throw_DateTimeException_on_failure(): void
{
$this->dateValidator = new DateTime('y-m-d');
$this->dateValidator->assert('2009-09-09');
}
/**
* @doesNotPerformAssertions
*/
public function testDateTimeExceptionalFormatsThatShouldBeValid(): void
{
$this->dateValidator = new DateTime('c');
$this->dateValidator->assert('2004-02-12T15:19:21+00:00');
$this->dateValidator = new DateTime('r');
$this->dateValidator->assert('Thu, 29 Dec 2005 01:02:03 +0000');
}
/**
* Test that datetime strings with timezone information are valid independent on the system's timezone setting.
*
* @doesNotPerformAssertions
*
* @param string $systemTimezone
* @param string $input
*
* @dataProvider providerForDateTimeTimezoneStrings
*/
public function testDateTimeSystemTimezoneIndependent($systemTimezone, $format, $input): void
{
date_default_timezone_set($systemTimezone);
$this->dateValidator = new DateTime($format);
$this->dateValidator->assert($input);
}
/**
* @return array
*/
public function providerForDateTimeTimezoneStrings()
public function providerForValidInput(): array
{
return [
['UTC', 'Ym', '202302'],
['UTC', 'Ym', '202304'],
['UTC', 'Ym', '202306'],
['UTC', 'Ym', '202309'],
['UTC', 'Ym', '202311'],
['UTC', 'c', '2005-12-30T01:02:03+01:00'],
['UTC', 'c', '2004-02-12T15:19:21+00:00'],
['UTC', 'r', 'Thu, 29 Dec 2005 01:02:03 +0000'],
['Europe/Amsterdam', 'c', '2005-12-30T01:02:03+01:00'],
['Europe/Amsterdam', 'c', '2004-02-12T15:19:21+00:00'],
['Europe/Amsterdam', 'r', 'Thu, 29 Dec 2005 01:02:03 +0000'],
['UTC', 'U', 1464658596],
['UTC', 'U', 1464399539],
['UTC', 'g', 0],
['UTC', 'h', 6],
['UTC', 'z', 320],
[new DateTime(), 'now'],
[new DateTime(), 'today'],
[new DateTime(), 'tomorrow'],
[new DateTime(), 'yesterday'],
[new DateTime(), '+1 day'],
[new DateTime(), 'next Thursday'],
[new DateTime(), '+1 week 2 days 4 hours 2 seconds'],
[new DateTime(), 2018],
[new DateTime(), new DateTimeMutable()],
[new DateTime(), new DateTimeImmutable()],
[new DateTime('Y-m-d'), '2009-09-09'],
[new DateTime('d/m/Y'), '23/05/1987'],
[new DateTime('c'), '2004-02-12T15:19:21+00:00'],
[new DateTime('r'), 'Thu, 29 Dec 2005 01:02:03 +0000'],
[new DateTime('U'), 1464658596],
[new DateTime('h'), 6],
[new DateTime('z'), 320],
[new DateTime('Ym'), 202302],
];
}
/**
* {@inheritdoc}
*/
public function providerForInvalidInput(): array
{
return [
[new DateTime(), 'not-a-date'],
[new DateTime(), []],
[new DateTime(), true],
[new DateTime(), false],
[new DateTime(), null],
[new DateTime(), ''],
[new DateTime('Y-m-d'), '2009-12-00'],
[new DateTime('Y-m-d'), '2018-02-29'],
[new DateTime('h'), 24],
[new DateTime('c'), new DateTimeMutable()],
[new DateTime('c'), new DateTimeImmutable()],
];
}
/**
* @test
*/
public function shouldPassFormatToParameterToException(): void
{
$format = 'F jS, Y';
$equals = new DateTime($format);
$exception = $equals->reportError('input');
self::assertSame($format, $exception->getParam('format'));
}
public function providerForDateTimeWithTimezone(): array
{
return [
['c', '2004-02-12T15:19:21+00:00', 'Europe/Amsterdam'],
['c', '2004-02-12T15:19:21+00:00', 'UTC'],
['d/m/Y', '23/05/1987', 'Europe/Amsterdam'],
['d/m/Y', '23/05/1987', 'UTC'],
['r', 'Thu, 29 Dec 2005 01:02:03 +0000', 'Europe/Amsterdam'],
['r', 'Thu, 29 Dec 2005 01:02:03 +0000', 'UTC'],
['Ym', '202302', 'Europe/Amsterdam'],
['Ym', '202302', 'UTC'],
];
}
/**
* Datetime strings with timezone information are valid independent on the
* system's timezone setting.
*
* @test
*
* @dataProvider providerForDateTimeWithTimezone
*
* @param string $format
* @param string $input
* @param string $timezone
*/
public function shouldValidateNoMatterTimezone(string $format, string $input, string $timezone): void
{
$currentTimezone = date_default_timezone_get();
date_default_timezone_set($timezone);
$rule = new DateTime($format);
self::assertTrue($rule->validate($input));
date_default_timezone_set($currentTimezone);
}
}