Last active
November 3, 2016 12:28
-
-
Save davidfuhr/3fd61f9591ee8bf30a00c674df680c82 to your computer and use it in GitHub Desktop.
DateInterval Doctrine Type and Symfony Validator Constraint
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* @copyright David Fuhr | |
* @license https://opensource.org/licenses/BSD-3-Clause BSD-3-Clause | |
*/ | |
declare(strict_types = 1); | |
namespace AppBundle\Validator\Constraints; | |
use Symfony\Component\Validator\Constraint; | |
/** | |
* @Annotation | |
*/ | |
class DateInterval extends Constraint | |
{ | |
const INVALID_FORMAT_ERROR = 'b3b282fd-6012-41a1-b2cb-14b96b644c92'; | |
protected static $errorNames = array( | |
self::INVALID_FORMAT_ERROR => 'INVALID_FORMAT_ERROR', | |
); | |
public $message = 'This value is not a valid ISO 8601 duration.'; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* @copyright David Fuhr | |
* @license https://opensource.org/licenses/BSD-3-Clause BSD-3-Clause | |
*/ | |
declare(strict_types = 1); | |
namespace AppBundle\Doctrine\Types; | |
use Doctrine\DBAL\Platforms\AbstractPlatform; | |
use Doctrine\DBAL\Types\StringType; | |
class DateIntervalType extends StringType | |
{ | |
const DATEINTERVAL = 'dateinterval'; | |
public function getName() | |
{ | |
return self::DATEINTERVAL; | |
} | |
public function convertToPHPValue($value, AbstractPlatform $platform) | |
{ | |
if (null !== $value) { | |
$value = new \DateInterval($value); | |
} | |
return $value; | |
} | |
public function convertToDatabaseValue($value, AbstractPlatform $platform) | |
{ | |
if ($value instanceof \DateInterval) { | |
$value = $value->format('P%yY%mM%dDT%hH%iM%sS'); | |
$value = str_replace(['M0S', 'H0M', 'DT0H', 'M0D', 'Y0M', 'P0Y'], ['M', 'H', 'DT', 'M', 'Y', 'P'], $value); | |
if ('T' === substr($value, -1)) { | |
$value = substr($value, 0, -1); | |
} | |
} | |
return parent::convertToDatabaseValue($value, $platform); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function requiresSQLCommentHint(AbstractPlatform $platform) | |
{ | |
return true; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* @copyright David Fuhr | |
* @license https://opensource.org/licenses/BSD-3-Clause BSD-3-Clause | |
*/ | |
declare(strict_types = 1); | |
namespace AppBundle\Tests\Doctrine\Types; | |
use AppBundle\Doctrine\Types\DateIntervalType; | |
use DateInterval; | |
use Doctrine\DBAL\Platforms\MySqlPlatform; | |
use Doctrine\DBAL\Types\Type; | |
class DateIntervalTypeTest extends \PHPUnit_Framework_TestCase | |
{ | |
/** | |
* @var DateIntervalType | |
*/ | |
private $type; | |
protected function setUp() | |
{ | |
if (false === Type::hasType(DateIntervalType::DATEINTERVAL)) { | |
Type::addType(DateIntervalType::DATEINTERVAL, DateIntervalType::class); | |
} | |
$this->type = Type::getType(DateIntervalType::DATEINTERVAL); | |
} | |
/** | |
* @dataProvider provideTimestampDateTime | |
*/ | |
public function testConvertToPhpValue($value) | |
{ | |
/* @var $actual DateInterval */ | |
$actual = $this->type->convertToPHPValue($value, new MySqlPlatform()); | |
$actual = $this->intervalToString($actual); | |
$expected = $value; | |
if (null !== $expected) { | |
$expected = new \DateInterval($value); | |
} | |
$expected = $this->intervalToString($expected); | |
$this->assertEquals($actual, $expected); | |
} | |
/** | |
* @dataProvider provideTimestampDateTime | |
*/ | |
public function testConvertToDatabaseValue($value) | |
{ | |
$actual = $value; | |
if (null !== $value) { | |
$actual = new \DateInterval($value); | |
} | |
$actual = $this->type->convertToDatabaseValue($actual, new MySqlPlatform()); | |
$this->assertEquals($value, $actual); | |
} | |
private function intervalToString(DateInterval $interval = null) | |
{ | |
if (null === $interval) { | |
return null; | |
} | |
return $interval->format('P%yY%mM%dDT%hH%iM%sS'); | |
} | |
public function provideTimestampDateTime() | |
{ | |
return [ | |
[ | |
'P1D', | |
], | |
[ | |
'PT1H2M', | |
], | |
[ | |
'P3Y6M4DT12H30M17S', | |
], | |
[ | |
'P2Y4DT6H8M', | |
], | |
[ | |
null, | |
], | |
]; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* @copyright David Fuhr | |
* @license https://opensource.org/licenses/BSD-3-Clause BSD-3-Clause | |
*/ | |
declare(strict_types = 1); | |
namespace AppBundle\Validator\Constraints; | |
use Symfony\Component\Validator\Constraint; | |
use Symfony\Component\Validator\ConstraintValidator; | |
use Symfony\Component\Validator\Context\ExecutionContextInterface; | |
use Symfony\Component\Validator\Exception\UnexpectedTypeException; | |
class DateIntervalValidator extends ConstraintValidator | |
{ | |
const PATTERN = '/^P(\d+Y)?(\d+M)?(\d+D)?(T(\d+H)?(\d+M)?(\d+S)?)?$/'; | |
/** | |
* Checks if the passed value is valid. | |
* | |
* @param mixed $value The value that should be validated | |
* @param Constraint $constraint The constraint for the validation | |
*/ | |
public function validate($value, Constraint $constraint) | |
{ | |
if (!$constraint instanceof DateInterval) { | |
throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\Date'); | |
} | |
if (null === $value || '' === $value || $value instanceof \DateInterval) { | |
return; | |
} | |
if (!is_scalar($value) && !(is_object($value) && method_exists($value, '__toString'))) { | |
throw new UnexpectedTypeException($value, 'string'); | |
} | |
$value = (string) $value; | |
if (!preg_match(static::PATTERN, $value, $matches)) { | |
if ($this->context instanceof ExecutionContextInterface) { | |
$this->context->buildViolation($constraint->message) | |
->setParameter('{{ value }}', $this->formatValue($value)) | |
->setCode(DateInterval::INVALID_FORMAT_ERROR) | |
->addViolation(); | |
} else { | |
$this->buildViolation($constraint->message) | |
->setParameter('{{ value }}', $this->formatValue($value)) | |
->setCode(DateInterval::INVALID_FORMAT_ERROR) | |
->addViolation(); | |
} | |
return; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* @copyright David Fuhr | |
* @license https://opensource.org/licenses/BSD-3-Clause BSD-3-Clause | |
*/ | |
declare(strict_types = 1); | |
namespace Symfony\Component\Validator\Tests\Constraints; | |
use AppBundle\Validator\Constraints\DateInterval; | |
use AppBundle\Validator\Constraints\DateIntervalValidator; | |
class DateIntervalValidatorTest extends AbstractConstraintValidatorTest | |
{ | |
/** | |
* This method is only here to get rid of the warning issued in the parent method | |
* | |
* @inheritdoc | |
*/ | |
protected function getMock($originalClassName, $methods = [], array $arguments = [], $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $cloneArguments = false, $callOriginalMethods = false, $proxyTarget = null) | |
{ | |
$mockObject = $this->getMockObjectGenerator()->getMock( | |
$originalClassName, | |
$methods, | |
$arguments, | |
$mockClassName, | |
$callOriginalConstructor, | |
$callOriginalClone, | |
$callAutoload, | |
$cloneArguments, | |
$callOriginalMethods, | |
$proxyTarget | |
); | |
$this->registerMockObject($mockObject); | |
return $mockObject; | |
} | |
protected function createValidator() | |
{ | |
return new DateIntervalValidator(); | |
} | |
public function testNullIsValid() | |
{ | |
$this->validator->validate(null, new DateInterval()); | |
$this->assertNoViolation(); | |
} | |
public function testEmptyStringIsValid() | |
{ | |
$this->validator->validate('', new DateInterval()); | |
$this->assertNoViolation(); | |
} | |
public function testDateIntervalClassIsValid() | |
{ | |
$this->validator->validate(new \DateInterval('P1D'), new DateInterval()); | |
$this->assertNoViolation(); | |
} | |
/** | |
* @expectedException \Symfony\Component\Validator\Exception\UnexpectedTypeException | |
*/ | |
public function testExpectsStringCompatibleType() | |
{ | |
$this->validator->validate(new \stdClass(), new DateInterval()); | |
} | |
/** | |
* @dataProvider getValidIntervals | |
*/ | |
public function testValidIntervals($interval) | |
{ | |
$this->validator->validate($interval, new DateInterval()); | |
$this->assertNoViolation(); | |
} | |
public function getValidIntervals() | |
{ | |
return array( | |
array('P1D'), | |
array('P12DT5H'), | |
array('PT12M'), | |
array('P3Y6M4DT12H30M17S'), | |
); | |
} | |
/** | |
* @dataProvider getInvalidIntervals | |
*/ | |
public function testInvalidIntervals($interval, $code) | |
{ | |
$constraint = new DateInterval(array( | |
'message' => 'myMessage', | |
)); | |
$this->validator->validate($interval, $constraint); | |
$this->buildViolation('myMessage') | |
->setParameter('{{ value }}', '"'.$interval.'"') | |
->setCode($code) | |
->assertRaised(); | |
} | |
public function getInvalidIntervals() | |
{ | |
return array( | |
array('foobar', DateInterval::INVALID_FORMAT_ERROR), | |
array('foobar P1DT2M', DateInterval::INVALID_FORMAT_ERROR), | |
array('P1MT2M foobar', DateInterval::INVALID_FORMAT_ERROR), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment