Symfony Advent Calendar 2018 7日目の記事(遅刻)です。
昨日も私の記事でした :sweat_smile:

巨大なプロジェクトの構成要素を小さなコンポーネントに分けて開発するようになると、バリデーションをSymfonyプロジェクト外で書くことも増えます。

こういうときSymfonyがコンポーネントごとに分かれていて、独立して使用することもできるのが生きてきます。
昔のsymfony1時代との大きな違いですね!

実際にSymfony Validatorでバリデーションしてみる

下記のような Term クラスがあるとします。

<?php

namespace Quartetcom\DecBlogDemo\Entity;

class Term
{
    /**
     * @var \DateTime
     */
    private $from;

    /**
     * @var \DateTime
     */
    private $to;

    public function __construct(\DateTime $from, \DateTime $to)
    {
        $this->from = $from;
        $this->to = $to;
    }

    public function validateLessThan90Days()
    {
        if ($this->to->diff($this->from)->days > 90) {
            throw new \LogicException('Term is too long.');
        }
    }

    // ...
}

このTermクラスのバリデーションをSymfonyのValidatorコンポーネントを使って書き換えてみましょう。

まず、composerで symfony/validator への依存を追加します。

$ composer require symfony/validator

そしてvalidateLessThan90days() メソッドをSymfonyのValidator用の書き方に変更します。

<?php

//...
+use Symfony\Component\Validator\Constraints as Assert;
+use Symfony\Component\Validator\ExecutionContextInterface;


class Term
{
    // ...

+    /**
+     * @Assert\Callback
+     */
+    public function validateLessThan90Days(ExecutionContextInterface $context)
+    {
+        if ($this->to->diff($this->from)->days > 90) {
+            $context->buildViolation('term.too_long')
+                    ->atPath('to')
+                    ->addViolation()
+            ;
+        }
+    }
-    public function validateLessThan90Days()
-    {
-        if ($to->diff($from)->days > 90) {
-            throw new \LogicException('Term is too long.');
-        }
-    }

    // ...
}

テストを書いて、このバリデーションが期待通りに動くかどうか確認してみましょう。

<?php


namespace Quartetcom\DecBlogDemo\Entity;


use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Validation;

class TermTest extends TestCase
{
    public function test_validate_90days()
    {
        $term = new Term(new \DateTime('2018-11-01'), new \DateTime('2019-01-30'));
        $this->assertValidTerm($term);
    }

    public function test_validate_91days()
    {
        $term = new Term(new \DateTime('2018-11-01'), new \DateTime('2019-01-31'));
        $this->assertInvalidTerm($term);
    }

    private function assertValidTerm(Term $term)
    {
        $this->assertEquals(0, $this->getViolationCount($term));
    }

    private function assertInvalidTerm(Term $term)
    {
        $this->assertGreaterThan(0, $this->getViolationCount($term));
    }

    /**
     * @param Term $term
     * @return int length of errors
     */
    private function getViolationCount(Term $term): int
    {
        $validator = Validation::createValidator();

        return count($validator->validate($term));
    }
}

2018-12-08 22 28 29

バリデーションが行われなかったようです… :cry:
おっと、Validator初期化時にアノテーションによるバリデーション設定を有効化する必要があるのを忘れていました。

<?php

namespace Quartetcom\DecBlogDemo;

// ...

class TermTest extends TestCase
{
    // ...

    private function getViolationCount(Term $term)
    {
-        $validator = Validation::createValidator();
+        $validator = Validation::createValidatorBuilder()->enableAnnotationMapping()->getValidator();

        return count($validator->validate($term));
    }
}

今度こそバリデーションできるはず!
2018-12-08 22 35 11

今度は doctrine/annotationsdoctrine/cache が必要だというエラーが出てしまいました。
メッセージに従い、追加します。

$ composer require doctrine/annotations doctrine/cache

今度こそバリデーションできるはず!
2018-12-08 22 38 15

Symfony\Component\Validator\Constraints\Callback がクラスとして読み込めていないようです :thinking:
AnnotationRegistryにautoloadできるクラスが存在すればAnnotationとして利用できる設定を追加します。
※ この設定は https://github.com/doctrine/annotations/issues/103 によるとDoctrine v3から不要になるようです。

<?php

namespace Quartetcom\DecBlogDemo;

// ...
+use Doctrine\Common\Annotations\AnnotationRegistry;


class TermTest extends TestCase
{
    // ...

    private function getViolationCount(Term $term)
    {
+        AnnotationRegistry::registerLoader('class_exists');
        $validator = Validation::createValidatorBuilder()->enableAnnotationMapping()->getValidator();

        return count($validator->validate($term));
    }
}

ついにバリデーションが動きました! :tada:
2018-12-08 22 44 17

まとめ

私はガッチリ密結合だったsymfony1.0の頃からsymfonyを使っているので、Symfonyのバージョンが1→2→3→4と進むにつれて、疎結合がどんどん加速しているのを感じます。
マイクロサービス志向・コンポーネント志向が進む中、安心して使うことができるSymfony Componentの役割はますます大きくなりそうです。

※ 本記事のサンプルコードを こちらのレポジトリ で公開しています。ご参考にどうぞ