symfony カスタムバリデーションの作り方

# はじめに
symfonyは非常に強力なformクラスを持ったフレームワークです。
案件で使いはじめたのですが、まとまった情報が少なく
(公式documentは豊富なんですが、最新は英語だけなのと必要な情報がどれかを調べるのが辛い)
備忘録も兼ねて、機能のまとめを作成してます。    
前回、前々回でformの作成とvalidationの記述の仕方を書きました。    

今回はvalidation ruleを作成することができるカスタムバリデーションのことについてまとめたいと思います。
## 環境
- symfony 2.8
- php 7.0

### 今まで書いたsymfonyの記事
- symfony でのform作成の基本
	- http://qiita.com/yutaChaos/items/0ae0d1797db4cb16466c
- symfony validation処理の方法
	- http://qiita.com/yutaChaos/items/95e70ef96608809bf475

## カスタムバリデーションの作り方
フォームやEntityは実装済みという前提で、日付のvalidationを作成していきます。   
3ステップです。便利!
### 1.  Constraintクラスの作成
- Symfony\Component\Validator\Constraintクラスを継承した制約クラスの作成

```php
<?php

namespace AppBundle\Validator\Constraint;

use Symfony\Component\Validator\Constraint;

/**
 * @Annotation
 */
class CheckDateFormat extends Constraint
{
    public $message = 'Invalid date format';

    public function __construct($message= "")
    {
        if(!empty($message['message'])){
            $this->message = $message['message'];
        }
    }
}
```

### 2.  ConstraintValidatorクラスの作成
- Symfony\Component\Validator\ConstraintValidatorクラスを継承したvalidatorクラスの作成

```php
<?php

namespace AppBundle\Validator\Constraint;

use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

class CheckDateFormatValidator extends ConstraintValidator
{
    public function validate($value, Constraint $constraint)
    {
        if(self::checkDatetimeFormat($value)){
            $this->context
                ->buildViolation($constraint->message)
                ->addViolation();
        }
    }

    public function checkDatetimeFormat($datetime)
    {
        if (!self::validateDate($datetime)) {
            return true;
        }

        return false;
    }

    private function validateDate($date, $format = 'Y-m-d')
    {
        try {
            \DateTime::createFromFormat($format, $date);
            $info = \DateTime::getLastErrors();
        }catch (Exception $e){
            return false;
        }
        return !$info['errors'] && !$info['warnings'];
    }
}
```

### 3. Validationの設定
- entityにannotionで付与する場合
	- 作成したConstraintのクラスをannotionで付与します。
例

```php
<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use AppBundle\Validator\Constraint as Custom;


/**
 * ToDo
 *
 * @ORM\Table(name="to_do")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\ToDoRepository")
 */
class ToDo
{

    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     * @ORM\Column(name="task", type="integer", length=1)
     */
    private $task;

    /**
     * @var string
     * @Assert\NotBlank()
     * @Assert\Length(min = 2,max = 255)
     * @ORM\Column(name="memo", type="string", length=255, nullable=true)
     */
    private $memo;

    /**
     * @var string
     * @Custom\CheckDateFormat()
     * @ORM\Column(name="date", type="datetime",nullable=false)
     */
    private $date;


    /**
     * @var \DateTime
     *
     * @ORM\Column(name="r_datetime", type="datetime", nullable=true)
     */
    private $rDatetime;

    /**
     * @var \DateTime
     *
     * @ORM\Column(name="u_datetime", type="datetime", nullable=true)
     */
    private $uDatetime;

    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set task
     *
     * @param string $task
     * @return ToDo
     */
    public function setTask($task)
    {
        $this->task = $task;

        return $this;
    }

    /**
     * Get task
     *
     * @return string 
     */
    public function getTask()
    {
        return $this->task;
    }

    /**
     * Set memo
     *
     * @param string $memo
     * @return ToDo
     */
    public function setMemo($memo)
    {
        $this->memo = $memo;

        return $this;
    }

    /**
     * Get memo
     *
     * @return date
     */
    public function getMemo()
    {
        return $this->memo;
    }


    /**
     * @return date
     */
    public function getDate()
    {
        return $this->date;
        return $this;
    }

    /**
     * @param string $date
     */
    public function setDate($date)
    {
        $this->date = $date;
    }
    /**
     * Set rDatetime
     *
     * @param \DateTime $rDatetime
     * @return ToDo
     */
    public function setRDatetime($rDatetime)
    {
        $this->rDatetime = $rDatetime;

        return $this;
    }

    /**
     * Get rDatetime
     *
     * @return \DateTime
     */
    public function getRDatetime()
    {
        return $this->rDatetime;
    }

    /**
     * Set uDatetime
     *
     * @param \DateTime $uDatetime
     * @return ToDo
     */
    public function setUDatetime($uDatetime)
    {
        $this->uDatetime = $uDatetime;

        return $this;
    }

    /**
     * Get uDatetime
     *
     * @return \DateTime
     */
    public function getUDatetime()
    {
        return $this->uDatetime;
    }

    /**
     * @ORM\PrePersist
     */
    public function refreshRDatetime()
    {
        $this->setRDatetime(new \Datetime());
    }

    /**
     * @ORM\PrePersist
     * @ORM\PreUpdate
     */
    public function refreshUDatetime()
    {
        $this->setUDatetime(new \Datetime());
    }

}
```

作成したConstraintのnamespaceに別名をつけて
```
use AppBundle\Validator\Constraint as Custom;
```

validationを付与したいメンバにannotionを付与しています。

```
    /**
     * @var string
     * @Custom\CheckDateFormat()
     * @ORM\Column(name="date", type="datetime",nullable=false)
     */
    private $date;
```


- FormTypeに付与する場合
	- 付与したいメンバーのOptionに設定したconstraintsに設定します。自分の設定したクラスを指定する以外は通常のvalidationをつける方法と変わりません。

例

```php
<?php

namespace AppBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type as InputType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints as Assert;
use AppBundle\Validator\Constraint as Custom;


class ToDoType extends AbstractType
{
    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('task',
                InputType\ChoiceType::class,
                [
                    'choices' => [
                        '1' => 'work',
                        '2' => 'hobby'
                    ],

                    'constraints' =>
                        [
                            new Assert\NotBlank()
                        ]
                ]
            )
            ->add('memo',
                InputType\TextareaType::class,
                [
                    'constraints' => [
                        new Assert\NotBlank(),
                        new Assert\Length(['min' => 2, 'max' => 255])
                    ]
                ])
            ->add('date',
                InputType\TextType::class,
                [
                    'constraints' => [
                        new Custom\CheckDateFormat(),
                    ]
                ]);

        $entity = $builder->getData();

        if(!$entity->getId()){
            $builder->add('Create', InputType\SubmitType::class,
                [
                    'attr' => [
                        'class' => 'btn btn-primary'
                    ]
                ]
            );
        }else{
            $builder->add('Edit', InputType\SubmitType::class,
            [
            'attr' => [
                'class' => 'btn btn-primary'
            ]
                ]
            );
        }
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\ToDo'
        ));
    }

    /**
     * {@inheritdoc}
     */
    public function getBlockPrefix()
    {
        return 'appbundle_todo';
    }


}
``` 

## 終わりに
- 通常のvalidationでもある程度のアプリケーションを作成することができますが、実際に利用していく中で特殊なvalidationが必要になることがままあると思います。    
symfonyは疎結合が強く意識されたフレームワークでvalidationに関してもsymfonyの思想に合わせて作っていくと、個々のvalidationが切り離され、テストやvalidationをテストしやすいコードになると思います。


### 参考URL
- http://symfony.com/doc/2.8/validation/custom_constraint.html