BEAR.SundayでDoctrine2のORMとSymfony2のValidatorを一緒に使ってみた（1）

BEAR.SundayでDoctrine2のORMを使ってみた
http://qiita.com/77web@github/items/7b67e490ac59dcab0ca8
の続き。

前回はDoctrine2のORMを使ってDBデータを取得するところまでで終わった。
せっかくだから、今度はDBにデータを保存してみたい。

AppResourceにonPost()を追加
-----------------------------

まずは前回のMy\NoteApp\Resource\App\Noteに新規登録用のonPost()を追加。

```php
// src/Resource/App/Note.php

namespace My\NoteApp\Resource\App;

use BEAR\Resource\ResourceObject;
use Doctrine\ORM\EntityManager;
use My\NoteApp\Entity\Note as NoteEntity;
use Ray\Di\Di\Inject;

class Note extends ResourceObject
{
    /**
     * @var EntityManager
     */
    private $entityManager;

    /**
     * @param EntityManager $entityManager
     * @Inject
     */
    public function __construct(EntityManager $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function onGet($id)
    {
        // onGet()は前回のままなので省略
    }

    public function onPost($body)
    {
        $note = new NoteEntity();
        $note->setBody($body);

        $this->entityManager->persist($note);
        $this->entityManager->flush();

        $this['note'] = [
            'id' => $note->getId(),
            'body' => $note->getBody(),
        ];

        return $this;
    }
}
```

実行してみる。

```bash
php bootstrap/contexts/api.php post 'app://self/note?body=hoge'
[BODY]
note => array(
    id 3
    body hoge
),

[VIEW]
{
    "note": {
        "id": 3
        "body": "hoge"
    },
    "_links": {
        "self": {
            "href": "http://localhost/app/note/?body=hoge"
        }
    }
}
```

やった！ちゃんと登録できた！

登録前にバリデーションを入れたい
----------------------------

今はまだappだけだけど、いずれWEBから叩くapiになる予定（夢は大きく！）なので変なデータが入らないようにしたい。
バリデーションをif,isset,is_null,…と手で書くのは地獄すぎるので、\Symfony\Component\Validatorを使うことにする。

ちなみにSymfony2のDIコンテナでは、こんな風にバリデーションができる。

```php
$note = new NoteEntity();
$note->setBody($body);

$violations = $container->get('validator')->validate($note);
// $violationsはイテレーターでcount()して0ならエラーは無いということ
```

つまり、この$container->get('validator')と同じものを、前回のEntityManager同様Providerから渡してやれば、同じように使えるんじゃないか？と想定。
なお、制約（何をエラーとして何を正当とするか）は、普通にSymfonyで使う時同様にエンティティのプロパティ又はgetterメソッドのアノテーションとして書きたい。

symfony/validatorを追加
------------------------

```bash
composer require symfony/validator:"~2.5"
```

ValidatorProviderを追加→登録→Inject
-----------------------------------

前回DoctrineORMProviderを書いた時と同様にValidatorProviderを新しく作って書く。
参考にしたのはSymfony\Component\ValidatorのREADME。
https://github.com/symfony/Validator#usage
たったこれだけでSymfonyのDIコンテナから取れるvalidatorと同じものが作れるらしい。便利！

アノテーションを使いたいので、アノテーションを使ったサンプルコードからvalidator作成部分をget()メソッド内にコピペ。

```php
// src/Module/Provider/ValidatorProvider.php

namespace My\NoteApp\Module\Provider;

use Ray\Di\ProviderInterface;
use Symfony\Component\Validator\Validation;

class ValidatorProvider implements ProviderInterface
{
    public function get()
    {
        return Validation::createValidatorBuilder()->enableAnnotationMapping()->getValidator();
    }
} 
```

前回同様、新しく作ったProviderはBEAR.SundayのDIに登録。

```php
// src/Module/AppModule.php
class AppModule extends AbstractModule
{
    public function configure()
    {
        // 最初から書いてあるinstall()や、前回追加したDoctrineORMProviderのbind()がここにあるはず

        // validatorをbind
        $this->bind('Symfony\Component\Validator\Validator\ValidatorInterface')->toProvider('My\NoteApp\Module\Provider\ValidatorProvider');
    }
}
```

前回のEntityManager同様、AppResourceにInjectしておく。
本当はvalidatorを使う予定があるのはpost/updateのみで、get/deleteは使わないんだけど、onPost()とonUpdate()だけに注入するのは後回し。

```php
// src/Resource/App/Note.php

use Symfony\Component\Validator\Validator\ValidatorInterface; //追加

class Note extends ResourceObject
{
    private $entityManager;

    private $validator;

    /**
     * @Inject
     */
    public function __construct(EntityManager $entityManager, ValidatorInterface $validator)
    {
        $this->entityManager = $entityManager;
        $this->validator = $validator; //追加
    }
}
```

entityに制約を書く
--------------------

前回作ったNoteエンティティのプロパティに制約を追記。
$bodyしか書くところが無いのでとりあえず$bodyをNotBlank（空白文字でない）にする。

```php
// src/Entity/Note.php

namespace My\NoteApp\Entity;

use Symfony\Component\Validator\Constraints as Assert;

/**
 * @Entity
 * @Table(name="note")
 */
class Note
{
    /**
     * @var int
     * @Id
     * @Column(type="integer")
     * @GeneratedValue
     */
    private $id;

    /**
     * @var string
     * @Column(type="text")
     * @Assert\NotBlank()
     */
    private $body;

    // getter,setterは省略
}
```

Note::onPost()にバリデーション処理を追加
--------------------------------------

```php
// src/Resource/App/Note.php

+use BEAR\Resource\Code; //追加

    /**
     * @param string $body
     * @return Note
     */
    public function onPost($body)
    {
        $note = new NoteEntity();
        $note->setBody($body);

        $violations = $this->validator->validate($note);
        if (0 !== count($violations)) {
            $errors = [];
            foreach ($violations as $violation) {
                /** @var \Symfony\Component\Validator\ConstraintViolation $violation */
                if (!isset($errors[$violation->getPropertyPath()])) {
                    $errors[$violation->getPropertyPath()] = [];
                }
                $errors[$violation->getPropertyPath()][] = $violation->getMessage();
            }
            $this['errors'] = $errors;
            // HTTPステータスコードの変更
            $this->code = Code::BAD_REQUEST;

            return $this;
        }

        $this->entityManager->persist($note);
        $this->entityManager->flush();

        $this['note'] = [
          'id' => $note->getId(),
          'body' => $note->getBody(),
        ];

        return $this;
    }
```

これでバリデーションが実行されるようになったはず！

POSTしてみる
--------------------

敢えてNotBlankに逆らったデータ（bodyを空白にしたデータ）をpostしてみる。

```bash
php bootstrap/contexts/api.php post 'app://self/note?body='
```

実行すると、期待に反して、バリデーションエラーではなくexceptionが出たことによるエラーorz

```
[Semantical Error] The annotation "@Entity" in class My\NoteApp\Entity\Note was never imported. Did you maybe forget to add a "use" statement for this annotation?
```

Doctrine系のアノテーションがひっかかっているっぽい。
Noteエンティティに、普段SymfonyのエンティティでやるようにDoctrine\ORM\Mappingのuseを追加して、ORM関係のアノテーションにORMというプリフィクスをつけてみる。

```php
// src/Entity/Note.php

use Doctrine\ORM\Mapping as ORM; 

/**
 * @ORM\Entity()
 * @ORM\Table(name="note")
 */
class Note
{
    // 省略
}
```

あら？結果的にSymfonyで使う時と大差なくなった。
この状態で再度POSTしてみる。

```bash
php bootstrap/contexts/api.php post 'app://self/note?body='
```

またもexception。
今度は違うメッセージが出た。

```
Class "My\NoteApp\Entity\Note" is not a valid entity or mapped super class. 
```

ORMプリフィクスつけないとバリデーションでひっかかり、ORMつけるとDoctrine側でエンティティとして正しくないと言われる。
デッドロック状態！

Googleで検索。
``symfony validator annotation reader doctrine entity error`` というキーワードで首尾よく同じ状況の人が見つかった。
Trouble with importing annotations
http://stackoverflow.com/questions/11910147/trouble-with-importing-annotations

```
I had a similar issue when using Silex, Doctrine2 and the Symfony-Validator.

The solution was to avoid using the SimpleAnnotationsReader and instead using the normal AnnotationReader (have a look at Doctrine\ORM\Configuration::newDefaultAnnotationDriver). You can then preface every entity with ORM\ (and of course inserting use Doctrine\ORM\Mapping as ORM; in every entity).
```

SimpleAnnotationReaderではなくAnnotationReaderを使えばいいらしい。

DoctrineORMProviderを修正
---------------------------

改めて前回~~コピペした~~書いたEntityManagerのインスタンスを作るための記述を振り返ると、

```php
$config = Setup::createAnnotationMetadataConfiguration(...);
```

か

```php
$entityManager = EntityManager::create(...);
```

のどっちかが怪しい。

まず ``\Doctrine\ORM\Tools\Setup::createAnnotationMetadataConfiguration()`` のソースを確認することにする。
https://github.com/doctrine/doctrine2/blob/master/lib/Doctrine/ORM/Tools/Setup.php#L69

``$useSimpleAnnotationReader = true`` という引数のデフォルトがtrueなので、これをfalseにしてやれば良さそう。

```php
// src/Module/Provider/DoctrineORMProvider

namespace My\NoteApp\Module\Provider;

use Doctrine\ORM\Tools\Setup;
use Doctrine\ORM\EntityManager;
use Ray\Di\ProviderInterface;
use BEAR\Sunday\Inject\AppDirInject;

class DoctrineORMProvider implements ProviderInterface
{
    use AppDirInject;

    public function get()
    {
        $config = Setup::createAnnotationMetadataConfiguration(array(__DIR__."/../Entity"), false, null, null, false);
        $conn = ...;
        $entityManager = EntityManager::create($conn, $config);

        return $entityManager;
    }
} 
```

再度POSTしてみる
--------------------

```bash
php bootstrap/contexts/api.php post 'app://self/note?body='
```

実行結果は

```
[BODY]
errors => array(
    body => array(
        This value should not be blank.,
    ),
),

[VIEW]
{
    "errors": {
        "body": [
            "This value should not be blank."
        ]
    },
    "_links": {
        "self": {
            "href": "http://localhost/app/note/?body="
        }
    }
}

```

バリデーションできた！

次への課題は下記の通り

* エラーメッセージが翻訳されてない
* validatorをコンストラクタでなくvalidatorを使うメソッドだけにInjectしたい

もうちょっと、続くんじゃよ…。