LoginSignup
15
15

More than 5 years have passed since last update.

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

Last updated at Posted at 2014-07-12

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

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

AppResourceにonPost()を追加

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

// 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;
    }
}

実行してみる。

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コンテナでは、こんな風にバリデーションができる。

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

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

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

symfony/validatorを追加

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()メソッド内にコピペ。

// 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に登録。

// 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()だけに注入するのは後回し。

// 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(空白文字でない)にする。

// 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()にバリデーション処理を追加

// 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してみる。

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というプリフィクスをつけてみる。

// src/Entity/Note.php

use Doctrine\ORM\Mapping as ORM; 

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

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

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のインスタンスを作るための記述を振り返ると、

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

$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にしてやれば良さそう。

// 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してみる

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したい

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

15
15
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
15