Skip to content

Instantly share code, notes, and snippets.

@davidfuhr
Last active November 3, 2016 12:28
Show Gist options
  • Save davidfuhr/3fd61f9591ee8bf30a00c674df680c82 to your computer and use it in GitHub Desktop.
Save davidfuhr/3fd61f9591ee8bf30a00c674df680c82 to your computer and use it in GitHub Desktop.
DateInterval Doctrine Type and Symfony Validator Constraint
<?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.';
}
<?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;
}
}
<?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,
],
];
}
}
<?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;
}
}
}
<?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