読者です 読者をやめる 読者になる 読者になる

ゆきばた

ゆきばたの果てしない戯言

Symfony2.7 カンマ区切りの数値をバリデーションする

Symfony2のお話です。

Symfony2には、デフォルトでバリデーションクラスがあり
「空チェック」や「数値チェック」「emailチェック」などができますが、

どうしても "1,3,4" のようなカンマ区切りでPOSTされた値に
バリデーションをかけたい場合はどうすればよいのか。

今回は、この例を挙げてSymfony2のカスタムバリデーションについてまとめます。


f:id:yukibata:20170129133355j:plain

まずは、既存のバリデーションから


FormのEntityクラスでアノテーションによるバリデーションをかけたい場合、
以下のように記述すると思います。

<?php
namespace BlogBundle\Form\Entity;

use Symfony\Component\Validator\Constraints as Assert;

class ProfileEntity
{
    /**
     * @Assert\NotBlank(
     *     message = "年齢が空です",
     *     groups = {"profile"}
     * )
     * @Assert\Type(
     *     type = "numeric",
     *     message = "年齢が不正です",
     *     groups = {"profile"}
     * )
     * @Assert\Range(
     *     min = 0,
     *     max = 120,
     *     minMessage = "年齢が不正です",
     *     maxMessage = "年齢が不正です",
     *     groups = {"profile"}
     * )
     */
    private $profileAge;

これはユーザが入力した年齢にバリデーションをかける場合の例ですが、

"NotBlank" とか "Type" とか "Range" などは、
既に存在するバリデーションにオプションを渡す感じで実施できるものです。


既に備わっているバリデーションの種類は

 バリデータリファレンス | Symfony2日本語ドキュメント

をご覧ください。


実態はソース的には

 /vendor/symfony/symfony/src/Symfony/Component/Validator/Constraints

にあります。


Constraintsは「制約」という意味ですが、
まぁ「バリデーションの種類」って捉えるくらいでOKです。

アノテーションで @Assert\XXXXX って書くと

/vendor/symfony/symfony/src/Symfony/Component/Validator/Constraints

の下にある XXXXX って名前のバリデーションクラスを探しに行くんですね。


以上が、バリデーションの基本的な仕組みですが

Symfony2が元々用意していたバリデーション以外を実施したい場合は
独自で実装する必要があります。

独自でバリデーションを設定する方法は2つ


どのように独自でバリデーションを用意するかですが
2パターンあります。

1つ目は、callback によるものです。
上記で指定したEntityクラスに callbackメソッドを準備して、
その中で、バリデーションを実施するものです。

ですが、
このやり方は、2つ以上の変数に対してバリデーションを実施するものと
捉えておいた方がいいかもしれません。

ケースとしては

 $foods, $drink の2つに対して
 ⇒ "フードとドリンクは、合わせて3つまでです"

とかです。

2つ目は、カスタムバリデーションクラスを作成するものです
この記事のメイントピックです。

ケースとしては

 $hobbyList に "1,3,4" が入ってくる。

これが、「カンマ区切りの数値であるバリデーションを作成したい」です。

カスタムバリデーションを作成する


カスタムバリデーションの作成ですが、
やることはシンプルです。

Symfony2が元々持っている
Constraint「制約(バリデーションの種類)」に1つ追加してあげる
ということです。

/vendor/symfony/symfony/src/Symfony/Component/Validator/Constraints

に追加してもいいですが、
vendor配下を追加すると、面倒なこともあるので
「明示的に作ったよ」ということがわかるように、
自分で作ったバンドル配下に設置するのがオススメです。(必須じゃない)


では、
いよいよ「カンマ区切りの数値をバリデーションする」クラスを設定してみます。

1. ディレクトリ作成

ここでは、
自分で作った BlogBundle の配下に作ることで進めます。

mkdir -p ./BlogBundle/Component/Validator/Constraints

  BlogBundle
    ∟Component
       ∟Validator
          ∟Constraints

のように、ディレクトリを作ります。
あえて、本家のvendor配下に似せて作ってあります。

あとは、「制約クラス」と「バリデーションクラス」の作成です

2. 制約クラスを作成

vi ./BlogBundle/Component/Validator/Constraints/CommaSeparateInteger.php

<?php

namespace BlogBundle\Component\Validator\Constraints;

use Symfony\Component\Validator\Constraint as BaseConstraint;

/**
 * @Annotation
 * @Target({"PROPERTY", "METHOD", "ANNOTATION"})
 *
 * @author
 **/

class CommaSeparateInteger extends BaseConstraint
{
    public $emptyMessage = '不正な入力があります'; 
    public $message = '不正な入力があります';
}

ここでやっていることは2つです。

1つ目は、Symfony2の Constraint クラスを継承していること

  use Symfony\Component\Validator\Constraint as BaseConstraint;
  class CommaSeparateInteger extends BaseConstraint

の部分ですね。これは必須の作業です。


2つ目は、エラーメッセージを定義していること。
これは、任意ですが、とりあえず

  public $message = '不正な入力があります';

くらいを設定して、後で変更しましょう。

3. バリデーションクラスを作成

vi ./BlogBundle/Component/Validator/Constraints/CommaSeparateIntegerValidator.php

<?php

namespace BlogBundle\Component\Validator\Constraints;

use Symfony\Component\Validator\Constraint as BaseConstraint;
use Symfony\Component\Validator\ConstraintValidator as BaseConstraintValidator;

class CommaSeparateIntegerValidator extends BaseConstraintValidator
{

    public function validate($value, BaseConstraint $constraint)
    {
        // 空っぽの場合
        if (empty($value)) {
            $this->context->addViolation($constraint->emptyMessage);
        }

        // カンマ区切りの数値であるか
        $trimmedValue  = preg_replace("/( | )/", "", $value);
        $values = explode(',', $trimmedValue);
        foreach ($values as $oneValue) {
            if (preg_match("/^[0-9]+$/", $oneValue)) continue;
            $this->context->addViolation($constraint->message);
        }
    }
}

ここでもやっていることは2つです。

1つ目は、ConstraintValidator クラスを継承していること

 use Symfony\Component\Validator\ConstraintValidator as BaseConstraintValidator;
 class CommaSeparateIntegerValidator extends BaseConstraintValidator

の部分ですね。必須作業です。
これを指定しないと、Entityで読み込んでくれません。


2つ目は、validate メソッドを作成していること

 public function validate($value, BaseConstraint $constraint)
 {
 }

の部分です。これも必須作業です。

validate メソッドは未実装メソッドですので、この名前でメソッドを生成してください。
で、実際のバリデーションの内容をこのメソッドに記載していきます。

ここまでで作成は一通り完成ですが、
「バリデーションがOKかNGかは、どのように表現するのか」も合わせて書いておきます。

Symfony2.3とかであれば、true false を返していたようですが、
Symfony2.5以上の場合、

 $this->context->addViolation($constraint->message);

によって「バリデーションに引っかかったよ」という信号を送ることができます。
つまり、validate メソッドが空っぽであれば「常にバリデーションOK」ということになります。


私の例では

 空判定  ⇒ addViolation($constraint->emptyMessage);
 数値判定 ⇒ addViolation($constraint->message);

のようにしています。

4. バリデーションクラスを使うように設定

あとは、作成したバリデーションクラスを実際に Entity から呼び出せばOKです。

<?php
namespace BlogBundle\Form\Entity;

use Symfony\Component\Validator\Constraints as Assert;
use BlogBundle\Component\Validator\Constraints as BlogAssert;

class ProfileEntity
{
    /**
     * @BlogAssert\CommaSeparateInteger(
     *     groups = {"profile"}
     * )
     */
    private $hobbyList;

上記のように
 
 use BlogBundle\Component\Validator\Constraints as BlogAssert;

を指定し、

 @BlogAssert\CommaSeparateInteger(

のような形式で利用することができます。


Symfony2.7の動きとしては

@Assert\xxxxx()
というアノテーションに出くわした際に、
Constraint配下にある「制約クラス名+Validator」という名前を見つけて、
その中の validate メソッドを実行という動きになっているんですね。



今回は、あくまで、どうしてもできない場合のカスタムバリデーションという話ですが
既存のバリデーションクラスも非常に豊富ですので、
やりたいバリデーションをしてくれるクラスが無いか、一度チェックしてみるといいですね。




おわり


[Image From]
http://loveendures.me/validation/