2013-10-23 16 views
2

我有一個理論實體如下描述:Symfony2的定製約束上重疊日期

company\MyBundle\Entity\ProgramGrid: 
    type: entity 
    table: program_grid 
    id: 
     id_program_grid: 
      type: integer 
      generator: {strategy: IDENTITY} 
    fields: 
     name: 
      type: text 
      nullable: true 
     start_date: 
      type: date 
      nullable: false 
     end_date: 
      type: date 
      nullable: true 

我woud喜歡添加驗證約束女巫驗證 起始日期日期和結束日期將不與其它記錄重疊。

如果我有2條A和B,我想:

B.start_date> A.end_date

什麼是實現這一目標的最佳途徑?

+0

您可以在文檔中找到一個好的開始:http://symfony.com/doc/current/cookbook/validation/custom_constraint.html – cheesemacfly

回答

0

您的問題的答案是事件。

你需要創建一個事件訂戶(如Symfony Docs描述)爲預堅持事件。

在該事件訂戶中,您必須查詢您的表並查看是否有重疊範圍。在這個問題的接受答案中找到了該算法的最佳答案:Determine Whether Two Date Ranges Overlap

+1

事件訂閱者可以拋出異常,但不能在表單中提供適當的反饋在表單中顯示的違反約束的情況。 – Jonny

0

我剛剛實現了這樣一個約束及其驗證器。這是什麼樣子:

約束:

<?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() 
     ; 
    } 
} 

你可以用一個例子實體in my gist一起找到它。