Skip to content

Instantly share code, notes, and snippets.

@foaly-nr1
Last active August 19, 2020 12:44
Show Gist options
  • Save foaly-nr1/52d066f458a23a88df8bd54f499d2ad9 to your computer and use it in GitHub Desktop.
Save foaly-nr1/52d066f458a23a88df8bd54f499d2ad9 to your computer and use it in GitHub Desktop.
NotOverlapping Symfony date period validator
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use AppBundle\Validator\Constraints\NotOverlapping;
/**
* @NotOverlapping("period")
*/
class CalendarTeamEvent
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @ORM\Embedded(class="League\Period\Period")
*/
protected $period;
}
<?php
namespace AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
/**
* @Annotation
*/
class NotOverlapping extends Constraint
{
public $message = 'This value overlaps with other values.';
public $service = 'app.validator.not_overlapping';
public $field;
public $errorPath;
public function getRequiredOptions()
{
return ['field'];
}
public function getDefaultOption()
{
return 'field';
}
/**
* The validator must be defined as a service with this name.
*
* @return string
*/
public function validatedBy()
{
return $this->service;
}
/**
* @return string
*/
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
}
<?php
namespace TriprHqBundle\Validator\Constraints;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Persistence\ManagerRegistry;
use League\Period\Period;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class NotOverlappingValidator extends ConstraintValidator
{
/**
* @var ManagerRegistry
*/
private $registry;
/**
* NotOverlappingValidator constructor.
* @param ManagerRegistry $registry
*/
public function __construct(ManagerRegistry $registry)
{
$this->registry = $registry;
}
/**
* @param object $entity
* @param Constraint $constraint
*
* @throws UnexpectedTypeException
* @throws ConstraintDefinitionException
*/
public function validate($entity, Constraint $constraint)
{
if (!$constraint instanceof NotOverlapping) {
throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\NotOverlapping');
}
if (!is_null($constraint->errorPath) && !is_string($constraint->errorPath)) {
throw new UnexpectedTypeException($constraint->errorPath, 'string or null');
}
$em = $this->registry->getManagerForClass(get_class($entity));
if (!$em) {
throw new ConstraintDefinitionException(sprintf('Unable to find the object manager associated with an entity of class "%s".', get_class($entity)));
}
/* @var $class \Doctrine\Common\Persistence\Mapping\ClassMetadata */
$class = $em->getClassMetadata(get_class($entity));
if (!array_key_exists($constraint->field, $class->embeddedClasses)) {
throw new ConstraintDefinitionException(sprintf(
'The field "%s" is not a Doctrine embeddable, so it cannot be validated for overlapping time periods.',
$constraint->field
));
}
$value = $class->reflFields[$constraint->field]->getValue($entity);
if (!is_null($value) && !($value instanceof Period)) {
throw new UnexpectedTypeException($value, 'null or League\Period\Period');
}
if(is_null($value)) {
return;
}
// ... WHERE existing_start < new_end
// AND existing_end > new_start;
$criteria = new Criteria();
$criteria
->where($criteria->expr()->lt(sprintf('%s.startDate', $constraint->field), $value->getEndDate()))
->andWhere($criteria->expr()->gt(sprintf('%s.endDate', $constraint->field), $value->getStartDate()))
;
$repository = $em->getRepository(get_class($entity));
$result = $repository->matching($criteria);
if ($result instanceof \IteratorAggregate) {
$result = $result->getIterator();
}
/* If no entity matched the query criteria or a single entity matched,
* which is the same as the entity being validated, there are no
* overlaps.
*/
if (0 === count($result) || (1 === count($result) && $entity === ($result instanceof \Iterator ? $result->current() : current($result)))) {
return;
}
$errorPath = $constraint->errorPath ?: $constraint->field;
$this->context->buildViolation($constraint->message)
->atPath($errorPath)
->addViolation()
;
}
}
# app/Resources/LeaguePeriod/doctrine/Period.orm.yml
League\Period\Period:
type: embeddable
fields:
startDate:
type: datetime
endDate:
type: datetime
# src/AppBundle/Resources/config/services.yml
services:
app.validator.not_overlapping:
class: AppBundle\Validator\Constraints\NotOverlappingValidator
arguments: ["@doctrine"]
tags:
- { name: validator.constraint_validator, alias: app.validator.not_overlapping }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment