‘doctrine’ タグのついている投稿

sfGuardFormSigninのメッセージをカスタマイズする

Symfony1.4でsfDoctrineGuardPluginを使っていて、ユーザ名かパスワードが違うときに、

The username and/or password is invalid.

と表示されてしまうので、これを変更したいと思います。それと、ラベルも日本語にしておきたいと思います。

sfGuardFormSigninを継承したフォームクラスをひとつ作ります。今回は/app/front/modules/sfGuardAuth/lib/form/mySigninForm.class.phpに作りました。

<?php

class mySigninForm extends sfGuardFormSignin
{
    public function configure()
    {
        parent::configure();
        
        $this->widgetSchema['username']->setLabel('ユーザID');
        $this->widgetSchema['password']->setLabel('パスワード');
        
        $this->validatorSchema['username']->setMessage('required', '入力して下さい');
        // $this->validatorSchema['username']->setMessage('invalid', 'ユーザIDかパスワードが違います'); // 効かない
        $this->validatorSchema['password']->setMessage('required', '入力して下さい');
        
        $validator = $this->getValidatorSchema()->getPostValidator();
        $validator->setMessage('invalid', 'ユーザIDかパスワードが違います');
    }
}

PostValidatorを取得してくるのが肝です。前述のメッセージはsfGuardValidatorUserで定義されているのですが、このバリデーターはBasesfGuardFormSigninでsetPostValidator(new sfGuardValidatorUser())として呼ばれているためです。

ちなみに、mySigninFormを有効にするには、/app/front/config/app.ymlに

all:
  sf_guard_plugin:
    signin_form: mySigninForm

を追加して、デフォルトのフォームクラスを変更する必要があります。


DoctrineMongoDBBundleでAbstractDoctrineExtensionのエラー

PHP Fatal error:  Class ‘Symfony\\Bridge\\Doctrine\\DependencyInjection\\AbstractDoctrineExtension’ not found in /home/path/to/Symfony/vendor/bundles/Symfony/Bundle/DoctrineMongoDBBundle/DependencyInjection/DoctrineMongoDBExtension.php on line 31

php – DoctrineMongoDBBundle getting a fatal error in Symfony2 – Stack Overflow

masterリポジトリが書き換えられていて、最新バージョンでは動かないのでdepsファイルにDoctrineMongoDBBundleのレポジトリを指定するときに、バージョン番号を指定する必要があるということでした。
DoctrineMongoDBBundleセクションにversion=v2.0.0を追加すればOKです。

[doctrine-mongodb]
    git=http://github.com/doctrine/mongodb.git

[doctrine-mongodb-odm]
    git=http://github.com/doctrine/mongodb-odm.git

[DoctrineMongoDBBundle]
    git=http://github.com/symfony/DoctrineMongoDBBundle.git
    target=/bundles/Symfony/Bundle/DoctrineMongoDBBundle
    version=v2.0.0


Symfonyでembed18nを使う

Symfonyで多言語対応する予定があるけど、とりあえず日本語だけ使いたかったときのメモです。embedI18nを自在に使う参考に。例によってSymfony 1.4+Doctrine。

// HogeForm.class.php
public HogeForm extends BaseHogeForm
{

    public function configure()
    {
        parent::configure();
        
        $this->embedI18n(array('ja'));
        
        // 二次元配列でembedしたフォームにアクセスできる
        $this->validatorSchema['ja']['name']->setOption('required', true);
        
        $this->useFields(array('ja', 'foo', 'bar'));
    }

}

このままテンプレートで$form->render()とかしてしまうと、入れ子表示になってしまうのでrenderRowでそれぞれ表示するようにしてやれば、useFieldsでembedされたフォームの順番がいじれないという問題も解決。

// hogeCreateSuccess.php
$form['ja']['name']->renderRow();
$form['foo']->renderRow();
$form['bar']->renderRow();

ちなみにsfFormDoctrine::getI18nFormClass()とかあるのですが、クラス名が取れるだけです。

追記

SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry ’19-ja’ for key ‘PRIMARY’

フォームをsaveしたら上記のエラーが出ました。

updateObject後にobject->toArray()をすると見覚えのない、「ja_JP」というキーが。

Array
(
    [id] => 
    [foo] => hoge
    [bar] => moge
    [Translation] => Array
        (
            [ja] => Array
                (
                    [id] => 
                    [name] => someone
                    [lang] => ja
                )
            [ja_JP] => Array
                (
                    [id] => 
                    [name] => 
                    [lang] => ja_JP
                )
        )
)

色々調べた結果、原因は

  • langがchar(2)なので、jaとja_JPの区別がつかないこと
  • sfDoctrineRecord::getDefaultCulture()がja_JPなこと

恐らく、

http://www.symfony-project.org/jobeet/1_4/Doctrine/ja/19

このページを参考にしているとハマるのではないかと…

ちなみにlangがchar(2)なのは、Doctrine_I18nの中で定義されています。

対策として考えられるのは以下のパターン。

  • sfDoctrineRecord::setDefaultCulture(‘ja’)をコールする
  • langをchar(5)にしてja_JPの形式で扱う
  • settings.ymlのdefault_cultureをjaにする
  • embedI18nにja_JPを渡す

sfDoctrineRecordのカルチャをセットするのは、sfUserのカルチャをセットすることでイベントが駆動して同時にセットできます。なので、$this->getUser()->setCulture(‘ja’)とかしておけばOK。

langをchar(5)にするのは、I18nビヘイビアのオプションにlength:5を指定すればできます。この場合、カルチャをすべて5文字で扱うようにしないとlangがマッチしなくなります。length:2の場合は暗黙的に先頭2文字だけにマッチさせることで5文字のカルチャを許容していたので。

# schema.yml
Sample:
  actAs:
    Timestampable: ~
    I18n:
      fields: [name]
      length: 5

settings.ymlのdefault_cultureをjaにするのが一番簡単です。en_USとen_GBはどうするんだという話ですけど…

formクラスに$this->embedI18n(array(‘ja_JP’))とすることでも対応できます。

気になる方はsfDoctrineRecordI18nFilterクラスのfilterSet関数を見てください。

カタログファイルとかにも影響しそうなのでsettings.ymlに2文字カルチャをセットするのがベターかなぁ。


symfonyで行の並び順を指定できるビヘイビアを使ってみた

今回はこちらを参考に、csDoctrineActAsSortablePluginを使ってみました。

$ ./symfony plugin:install csDoctrineActAsSortablePlugin
>> plugin    installing plugin "csDoctrineActAsSortablePlugin"
>> sfPearFrontendPlugin downloading csDoctrineActAsSortablePlugin-1.5.1.tgz ...
>> sfPearFrontendPlugin Starting to download csDoctrineActAsSortablePlugin-1.5.1.tgz
>> sfPearFrontendPlugin (6,795 bytes)
>> sfPearFrontendPlugin .
>> sfPearFrontendPlugin .
>> sfPearFrontendPlugin ...done: 6,795 bytes
>> sfSymfonyPluginManager Installation successful for plugin "csDoctrineActAsSortablePlugin"
>> sfSymfonyPluginManager Installing web data for plugin

使い方はschema.ymlのモデルのActAsにSortableを追加してやればいいだけです。

ModelName:
  actAs: 
    Timestampable: ~
    Sortable: ~

これで、自動的にpositionというbigintのカラムが生成されて、ソート順が数字で指定できるようになります。

併せて順序を入れ替えるための便利なメソッドが使えるようになります。

http://www.symfony-project.org/plugins/csDoctrineActAsSortablePlugin/1_5_1?tab=plugin_readme

今回、I18nの下にSortableを置いたら、インデックス名がやたら長くなってしまい、CREATE文でエラーが出るようになってしまいました。

SQLSTATE[42000]: Syntax error or access violation: 1059 Identifier name '******_*********_***_*******_******_*****_translation_position_sortable_idx' is too long.

plugins/csDoctrineActAsSortablePlugin/lib/template/Sortable.php

  protected function getSortableIndexName()
  {
    return sprintf('%s_%s_%s', $this->getTable()->getTableName(), $this->_options['name'], $this->_options['indexName']);
  }

ということなので、nameとindexNameを指定して短くできそうです。

と言ってもposition_sortable分しか短くできないのですけど。

最終的には、schema.ymlの方でtableNameをdatabase_name.table_nameで指定していたのですが、これをtableName: table_nameにするだけで対応できました。


MySQL Workbenchからschema.ymlを生成する

MySQL Workbenchからsymfony+Doctrine用のschema.ymlを書き出せないかなぁとつぶやいたら@hidenorigotoさんに教えていただいたので試してみました。
http://twitter.com/#!/hidenorigoto/status/38153994970988544

MySQL Workbench schema exporter

MySQL Workbench schema exporterは、MySQL WorkbenchプラグインのMySQL Workbench Doctrine Pluginにインスパイアを受けて開発されたようです。
が、こちらのプラグインはすでにメンテされていないようです。

開発終了した理由がぞろぞろ書いてありますが、要約すると「LUAが…」ってことみたいです。

MySQL Workbench schema exporterはPHPで書かれています。MySQL Workbenchのmwbファイルを読み込んでパース、フォーマッタを指定してそれぞれの形式を書き出すようです。
ちなみにmwbファイルはZIP圧縮されたXMLです。あと、SQLite3のデータが入ってました。

注意点としてPHP 5.3以降でないと実行できません…
いつものdebian環境では5.2を使ってるので、Windows版の5.3をダウンロードしてインストールしました。

まずはexampleディレクトリに移動します。

C:\Users\test\Downloads\johmue-mysql-workbench-schema-exporter-7d08e29\example>php -v 
PHP 5.3.5 (cli) (built: Jan  5 2011 20:36:18) 
Copyright (c) 1997-2010 The PHP Group 
Zend Engine v2.3.0, Copyright (c) 1998-2010 Zend Technologies

example/data/にtest.mwbがあり、サンプルではこのファイルを使います。

ちなみにサンプルのEER(Enhanced Entity-Relationship) diagramはこんな感じです。

test.mwb

C:\Users\test\Downloads\johmue-mysql-workbench-schema-exporter-7d08e29\example>php .\doctrine1.yaml.php 
<textarea cols="100" rows="50">Bureau: 
  tableName: mydb.bureaus 
  columns: 
    id: 
      type: integer(4) 
      primary: true 
      notnull: true 
      autoincrement: true 
    room: 
      type: string(45) 
  indexes: 
    testIndex: 
      fields: [room] 
      type: unique 
  options: 
    charset: utf8 
    type: InnoDB

Email: 
  actAs: 
    timestampable: 
  tableName: mydb.emails 
  columns: 
    id: 
      type: integer(4) 
      primary: true 
      notnull: true 
      autoincrement: true 
    email: 
      type: string(255) 
    users_id: 
      type: integer(4) 
      primary: true 
      notnull: true 
  relations: 
    User: 
      class: User 
      local: users_id 
      foreign: id 
      foreignAlias: Emails 
      onDelete: no action 
      onUpdate: no action 
  indexes: 
    fk_Emails_Users: 
      fields: [users_id] 
  options: 
    charset: utf8 
    type: InnoDB

User: 
  actAs: 
    timestampable: 
  tableName: mydb.users 
  columns: 
    id: 
      type: integer(4) 
      primary: true 
      notnull: true 
      autoincrement: true 
    name: 
      type: string(255) 
  options: 
    charset: utf8 
    type: InnoDB

UsersBureaus: 
  tableName: mydb.users_bureaus 
  columns: 
    users_id: 
      type: integer(4) 
      primary: true 
      notnull: true 
    bureaus_id: 
      type: integer(4) 
      primary: true 
      notnull: true 
  relations: 
    User: 
      class: User 
      local: users_id 
      foreign: id 
      foreignAlias: UsersBureauss 
      onDelete: no action 
      onUpdate: no action 
    Bureau: 
      class: Bureau 
      local: bureaus_id 
      foreign: id 
      foreignAlias: UsersBureauss 
      onDelete: no action 
      onUpdate: no action 
  indexes: 
    fk_users_bureaus_bureaus1: 
      fields: [bureaus_id] 
  options: 
    charset: utf8 
    type: InnoDB

Testtable: 
  tableName: mydb2.testtable 
  columns: 
    id: 
      type: integer(4) 
      primary: true 
      notnull: true 
  options: 
    charset: utf8 
    type: InnoDB 
</textarea><br><br>1 MB used<br>0.055 sec needed

というわけで、リレーションやインデックスなども書きだされるようです。

HTMLのタグはdoctrine1.yaml.phpの中に書いてあるので、CLIから使うときは適当に外してください。

timestampableなどのビヘイビアに関しては、コメント欄に書くことで出力できるようです。

{d:actAs} 
  actAs: 
    timestampable: 
{/d:actAs}

この仕様はちょっと微妙な気が…

created_atとupdated_atがあったらtimestampable付けるとかの方が楽だなぁ、と。

作っちゃった後の全テーブルにコメント指定して回るのはちょっと辛いので。

サーバにPHP 5.3が入っているなら、symfony taskでmwbからschema.ymlの生成と、ActAsの追加をやってしまうのがいいかな、と思いました。

うーん、個人的にはMySQL Workbench上で完結したいから、プラグインの方が便利な気はするんですけどねぇ。LUAとは言わなくてもPythonスクリプティング対応しているようなので、Pythonに書き直すとか…

この後、60テーブルほどのファイルを変換してみましたが、問題なく実行できました。

追記(2011/02/19)

注意点がいくつかあります。

というか、致命的かも。

1.NOT NULLのchar型にDEFAULT値”を入れるとNULLに変換される

これはDoctrineのbuild –sqlがおかしいかな…

column_name:

  type: char(3)

  notnull: true

  default: ”

column_name char(3) DEFAULT NULL NOT NULLとかいう矛盾を持ったクエリが生成されます。

ちなみにvarcharは大丈夫です。

default: ”の行は削除しましょう。

2.DECIMALやFLOATの最大桁数と少数点の値が逆になる

ひどい。

例えばMySQL WorkbenchでDECIMAL(15,4)と指定したカラムがDECIMAL(4,15)と書き出されます。

column_name:

  type: decimal(4,15)

  notnull: true

  default: ‘0.0000’

もちろん、ちゃんとMySQL Workbenchからクエリのエクスポートをしたときは15,4になります。

置換で何とか…

うーん、やっぱりMySQL WorkbenchでDBをあらかじめ作成しておいてから→doctrine:build-schema→schema.yml修正が妥当かなぁ。


admin generatorの編集後のリダイレクト先変更

admin generatorのeditアクションで、編集後に再度編集画面ではなく、リストに飛ばしたい要件があったのでメモ。例によってSymfony 1.4+Doctrine+admin generator。

class hogeActions extends autoHogeActions
{
    public function executeUpdate(sfWebRequest $request)
    {
        $this->forward404Unless($request->isMethod('put'));
        
        $this->hoge = $this->getRoute()->getObject();
        $this->form = $this->configuration->getForm($this->hoge);
        
        if ($this->form->bindAndSave($request->getParameter($this->form->getName()), $request->getFiles($this->form->getName()))) {
            $this->redirect('hoge');
        }
        
        $this->setTemplate('edit');
    }
}

executeUpdateをオーバーライドすればOKのようです。ここではisMethodがputなことに注意。


Symfonyでのsave時に独自の処理を挟む

Symfony 1.4+Doctrineで、admin generatorな環境です。editなど、自動生成なので、どこをオーバーライドすれば目的の動作になるのかわかりにくいことがありますね。

例えば、あるモデルのステータスを変更すると同時に、他のモデルに対しても操作したい場合があるとします。

まぁ、mergeFormとかembedFormとか使ってもいいのですが、今回は他テーブルのプライマリキーをフォームから選択させたかったので、汎用性のあるdoSaveでの処理を追加してみました。

symfony Forms in Action | 第11章 – Doctrine との統合 | symfony | Web PHP Framework

class BackendHogeForm extends HogeForm
{
    public function configure()
    {
        parent::configure();

        // 使用するフィールドを指定
        $this->useFields(array('status'));
        
        // 別モデルのIDリストを選択するフォームを作成する
        $choices = array(100 => '100:value1', 101 => '101:value2');
        $this->widgetSchema['other_model_id'] = new sfWidgetFormSelect(array('choices' => $choices));
        $this->validatorSchema['other_model_id'] = new sfValidatorChoice(array('choices' => array_keys($choices)));
    }

    protected function doSave($con = null)
    {
        if ($this->getValue('status') == 'DONE') {
            Doctrine::getTable('OtherModel')->find($this->getValue('other_model_id'))
                ->setIsSelected(TRUE)
                ->save();
        }
        return parent::doSave($con);
    }
}

基本的にはdoSaveをオーバーライドするだけです。ちなみにuseFieldsメソッドを使うと必要なフィールド以外を全部unsetしてくれるので便利です。


PHPのDateTimeの結果が-0001-11-30 00:00:00になる現象について

mySQLなどで、日時を0000-00-00 00:00:00としてデータベースに格納しておくことがありますが、これを読み込んでそのままPHPのDateTimeオブジェクトに渡すと、出力が-0001-11-30 00:00:00になってしまいます。

$a = new DateTime('0000-00-00 00:00:00');
echo $a->format('Y-m-d H:i:s');
// Output: -0001-11-30 00:00:00

PHP :: Bug #42971 :: DataTime::format(): not well formated data ‘0000-00-00 00:00:00’

で、これはバグではないと言われているので、どういうことかと考えてみると、0000-00-00は存在しない0月0日を指定しているので、0月は繰り下がって-1年12月0日、さらに0日も繰り下がって-1年11月30日、となるわけですね。

データベースとPHPの文化の違い、というところでしょうか。

ちなみにDateTime型、コンストラクタにNULLを渡すと現在時刻のインスタンスが生成されるので、データベースの値をNULLにしておくと、現在時刻になってしまいます。うーん…symfonyのDoctrineでNULL判定したい場合はどうすればいいんだ…

$row->getDateTimeObject('deleted_at')

みたいなことがやりたいのですが。

sfDoctrineRecordも

    $type = $this->getTable()->getTypeOf($dateFieldName);
    if ($type == 'date' || $type == 'timestamp')
    {
      return new DateTime($this->get($dateFieldName));
    }

こうなってるからオーバーライドするしかないのかな。


Doctrine_Collectionをループする

Symfony 1.4+Doctrine
やたら不便(だと個人的には思っている)なDoctrineの、SELECTに関するメモ。

やりたかったことは、ある条件で抽出した複数行に1行ずつ処理を加えて書き戻すというフロー。
fetchArray()とかでは配列しか返ってこないので意味がなく、fetchOne()では先頭行しか返ってこない。
fetchOne()を複数回実行すればいいのかと思ったら無限ループに陥った。

findByではorderByをするのにフックを使わないといけないらしいのでパス。

一応、以下の方法で解決しました。

$collection = Doctrine_Query::create()
    ->select('u.*')
    ->from('Users u')
    ->where('u.flag = ?', '1')
    ->orderBy('u.id')
    ->execute();

var_dump(get_class($collection)); // Doctrine_Collection

$iter = $collection->getIterator();
var_dump(get_class($iter)); // ArrayIterator

while ($record = $iter->current()) {
    var_dump(get_class($record)); // Users
    var_dump($record->getId());
    $iter->next();
}

next()呼ばないといけないのは面倒だなと思ったら、foreachで使えたらしい。

$collection = Doctrine_Query::create()
    ->select('u.*')
    ->from('Users u')
    ->where('u.flag = ?', '1')
    ->orderBy('u.id')
    ->execute();

var_dump(get_class($collection)); // Doctrine_Collection

$iter = $collection->getIterator();
var_dump(get_class($iter)); // ArrayIterator

foreach ($iter as $record) {
    var_dump(get_class($record)); // Users
    var_dump($record->getId());
}

これでかなりマシになった。

最終的にはこれで。

$collection = Doctrine_Query::create()
    ->select('u.*')
    ->from('Users u')
    ->where('u.flag = ?', '1')
    ->orderBy('u.id')
    ->execute();

var_dump(get_class($collection)); // Doctrine_Collection

foreach ($collection->getIterator() as $record) {
    var_dump(get_class($record)); // Users
    var_dump($record->getId());
}

あとはループ内で$record[‘data’] = ‘hoge’;$record->save();とかしておけばOK。

追記:2010/03/30

全然違った。もっと簡単にできました。getIterator不要でした。

foreach ($collection as $record) {
    $record['data'] = 'hoge';
    $record->save();
}

慣れてくると意外と便利な気がしなくもないDoctrine。結合系と特に。