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したい
もうちょっと、続くんじゃよ…。