SilexだってEntityクラス作ってClassMetadata設定して、制約による検証する


## はじめに
データベースの型、属性などの制約は、Entityクラス作ってそこにメタ情報記述したいけど、[Doctrine ORM](http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/)使ってないとそのようなことはできないとずっと思っていました。
が、試したところ、Entityクラス作ってClassMetadata設定して、制約による検証をすることができました。
自分なりに感じたメリットがあったので、紹介したいと思います。

## 環境
- PHP 7.0.8
- Silex 2.0.2
- Symfony 3.0.8

プロジェクトは、[Silex-Skeleton](https://github.com/silexphp/Silex-Skeleton)をベースに作成していることを前提とします。
未確認ですが、上記以外のバージョンでも利用できると思われます。
しかし、Formコンポーネントが、Symfonyのバージョンによって微妙に、インターフェイスが異なりますので、注意が必要です。
Silex2.0へのマイグレーションについては、[こちら](http://qiita.com/imunew/items/8b426749dd2d0d1eec1e)を参照ください。

## Entityクラスの作成
データベースのテーブルに対応するEntityクラスを作ってみます。
`loadValidatorMetadata`の部分は後ほど、詳しく説明します。

```php
namespace App/Entity;

use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Regex;
use Symfony\Component\Validator\Mapping\ClassMetadata;

class Member
{
    public $id;
    public $email;
    public $nickName;

    public static function loadValidatorMetadata(ClassMetadata $metadata)
    {
        $metadata->addPropertyConstraints('email', [
            new NotBlank(),
            new Email()
        ]);

        $metadata->addPropertyConstraints('nickName', [
            new NotBlank(),
            new Length(20),
            new Regex(['pattern' => '/[0-9a-zA-Z\-_]/'])
        ]);

    }
}
```

`id`に対する制約を設定していないのは、autoincrementで自動的に生成されることを想定しているためですが、万が一のために、Range制約で1以上を設定してもいいかもしれません。


## FormTypeのdata_classを設定

```php
namespace App\Form\Type;

use App\Entity\Member;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class MemberType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('id', HiddenType::class, [
        ]);

        $builder->add('email', EmailType::class, [
            'label' => 'Eメール'
        ]);

        $builder->add('nickName', TextType::class, [
            'label' => 'ニックネーム'
        ]);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Member::class
        ]);
    }    
}
```

## どのように入力検証されるか
コールスタックを時系列の早い順に並べると、下記のようになります。

- Symfony\Component\Form\Form::handleRequest
- Symfony\Component\Form\Form::submit
- Symfony\Component\EventDispatcher\ImmutableEventDispatcher::dispatch
- Symfony\Component\EventDispatcher\EventDispatcher::dispatch
- Symfony\Component\EventDispatcher\EventDispatcher::doDispatch
- call_user_func
- Symfony\Component\Form\Extension\Validator\EventListener- \ValidationListener::validateForm
- Symfony\Component\Validator\Validator\RecursiveValidator::validate
- Symfony\Component\Validator\Validator\RecursiveContextualValidator::validateObject
- Symfony\Component\Validator\Validator\RecursiveContextualValidator::validateClassNode
- Symfony\Component\Validator\Validator\RecursiveContextualValidator::validateInGroup
- Symfony\Component\Form\Extension\Validator\Constraints\FormValidator::validate
- Symfony\Component\Validator\Validator\RecursiveContextualValidator::validate
- Symfony\Component\Validator\Validator\RecursiveContextualValidator::validateObject
- Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory::getMetadataFor
- Symfony\Component\Validator\Mapping\Loader\StaticMethodLoader::loadClassMetadata
- ReflectionMethod::invoke
- App\Entity\Member::loadValidatorMetadata

`StaticMethodLoader`を見ると、'loadValidatorMetadata'という関数名がコンストラクタのデフォルト引数になっていることや、`ReflectionClass`が使われていることがわかり興味深いです。

[StaticMethodLoader](https://github.com/symfony/validator/blob/master/Mapping/Loader/StaticMethodLoader.php)

```php
<?php
namespace Symfony\Component\Validator\Mapping\Loader;
use Symfony\Component\Validator\Exception\MappingException;
use Symfony\Component\Validator\Mapping\ClassMetadata;

class StaticMethodLoader implements LoaderInterface
{
    /**
     * The name of the method to call.
     *
     * @var string
     */
    protected $methodName;
    /**
     * Creates a new loader.
     *
     * @param string $methodName The name of the static method to call
     */
    public function __construct($methodName = 'loadValidatorMetadata')
    {
        $this->methodName = $methodName;
    }
    /**
     * {@inheritdoc}
     */
    public function loadClassMetadata(ClassMetadata $metadata)
    {
        /** @var \ReflectionClass $reflClass */
        $reflClass = $metadata->getReflectionClass();
        if (!$reflClass->isInterface() && $reflClass->hasMethod($this->methodName)) {
            $reflMethod = $reflClass->getMethod($this->methodName);
            if ($reflMethod->isAbstract()) {
                return false;
            }
            if (!$reflMethod->isStatic()) {
                throw new MappingException(sprintf('The method %s::%s should be static', $reflClass->name, $this->methodName));
            }
            if ($reflMethod->getDeclaringClass()->name != $reflClass->name) {
                return false;
            }
            $reflMethod->invoke(null, $metadata);
            return true;
        }
        return false;
    }
}
```

## 雑感
僕が感じたメリットとしては、以下のようなことが挙げられます。

- `FormType`の`buildForm`にて、'constraints'オプションをダラダラと書かなくて済む
- `Doctrine\DBAL`で結果セットに`Entity`をフェッチすると、Formにsubmitされたデータをそのまま扱うことができる

以下は、個人的な設計指針みたいなものです。

- Entityの複数フィールドにまたがった制約は、Callback制約を使えば、Entityにも記述できるが、Entityへの制約は、あくまで普遍的なものに限定し、FormTypeに記述したほうがいい
- 重複データを事前に検証したい場合、FormTypeで検証コードを実装しようとすると、DB操作クラスをFormTypeに持ち込むことになるので、Formをビルドした側のどこかで検証したほうがいい

  ```php
    private function getForm(FormFactoryInterface $formFactory, Member $member = null)
    {
        return $formFactory->createBuilder(MemberType::class, $member)
            ->addEventListener(FormEvents::POST_SUBMIT, [$this, 'onSubmit'])
            ->getForm();
    }

    /**
     * onSubmit
     * @param FormEvent $formEvent
     * @param $eventName
     * @param EventDispatcherInterface $eventDispatcher
     */
    public function onSubmit(FormEvent $formEvent, $eventName, EventDispatcherInterface $eventDispatcher)
    {
        $form = $formEvent->getForm();
        $data = $formEvent->getData();
        
        /** 省略 */
    }
  ```

- なるべく`$form->isValid()`で検証は完結させたい

## おわりに
もうSymfonyでいいじゃんというツッコミはあるかと思いますが、Silexに必要な機能を少しずつ足していくのは、Symfonyそのものの勉強というか理解につながる気がしてます。
Silex1.2系（Symfony2.6まで）で同じことができるか、今回試せなかったので、どなたかよろしくお願いいたします。