July 28, 2008

symfony1.1でsfGuardPluginを使う。パート1

ならべて.comは、symfonyで開発された仕組みですが、1.0を使用しています。先日、ブログに貼り付けることができるウィジェットをリリースしており、もちろん開発は続けているのですが、現在その他に、別の仕組みを開発しており、そこでは、1.1を採用することにしました。symfony自体に関しては、もちろん根本にある使い方は変わっていないのですが、それなりに苦労しましたので、久しぶりにPHPネタでブログを書いてみます。

コードをできるだけ書かないことは、よりシンプルな開発となり、また、それがバグを減らすことなり重要ですよね。そこで、今回の開発では、アドミンジェネレータやプラグインを採用しようと決めました。プラグインでは、symfonyの開発者でもあるFabienさんのsfGuardPluginを使用しました。インストールできない人は、マニュアルを読んでください。

しかし、実際にsymfonyを使用して開発している方に話を聞いたのですが、実際のアプリとして作り込むには、sfGuardPluginは使いにくいので、アプリの作り方として勉強するならいいという話でした。今から書こうとする内容としては、いきなりこんなことを言うなんて凹んでしまいますが、そこは、よりコードを書かないようにゴリ押しで進めてみました。

さて、sfGuardPluginでは、ユーザの持つ情報をsf_guard_userテーブルに保存し、sfGuardUserというモデルで管理しています。このモデルのyamlスキーマは以下のようになっています。

  1. sf_guard_user:
  2.     _attributes:    { phpName: sfGuardUser }
  3.     id:             ~
  4.     username:       { type: varchar, size: 128, required: true, index: unique }
  5.     algorithm:      { type: varchar, size: 128, required: true, default: sha1 }
  6.     salt:           { type: varchar, size: 128, required: true }
  7.     password:       { type: varchar, size: 128, required: true }
  8.     created_at:     ~
  9.     last_login:     { type: timestamp }
  10.     is_active:      { type: boolean, required: true, default: 1 }
  11.     is_super_admin: { type: boolean, required: true, default: 0 }

sfGuardPluginに付いてくるsfGuardUserモジュールによって、sf_guard_userだけのCRUDは可能です。しかし、ユーザの属性には、もっといろいろな情報を持たせて、同時に登録したり、修正したいですよね?例えば、メールアドレスだったり、住所だったり、生年月日だったり、と。こういうときにあるのが、sf_guard_user_profileテーブルです。自分のスキーマファイルにsf_guard_user_profileを好きなカラムで指定することができます。そうすると、アクションクラスの中で$this->user->getProfile()と、持ってくることができます。つまり、テーブルが二つになって、1対1のリレーションで構成を作ってくれます。個人的にはこの1対1のリレーションが嫌いなのですが、プラグインの中を変更することは嫌ですので、このsf_guard_user_profileを使用しようと思います。しかし、やはり1対1のためか、アドミンジェネレータや、1.1から変更のあったフォーム周りを使用しようとすると結構大変でした。というわけで、ゴリ押しです。

さて、今開発しているものをそのまま持ってきてしまうと、説明がややこしくなったり、権利関係で問題になりそうですので、簡略化したモデルを使用しましょう。例えば、次のようなものです。

  1. sf_guard_user_profile:
  2.     _attributes: { phpName: sfGuardUserProfile }
  3.     id:
  4.     user_id:
  5.       type: integer
  6.       required: true
  7.       foreignTable: sf_guard_user
  8.       foreignReference: id
  9.       onDelete: cascade
  10.       onUpdate: cascade
  11.     name:
  12.       type: varchar(64)
  13.       required: true
  14.     birthday:
  15.       type: date
  16.     created_at:
  17.     updated_at:

準備は整いました。というわけで、ようやく本題。今回のsfGuardPluginネタは、二つのポストに分けて書こうと思います。前半のこのポストでは、symfony1.1のsfGuardPluginを、アドミンジェネレータを使用する方法について書きます。後半の次のポスト(予定)では、同様の環境をアドミンジェネレータではなく、symfony1.1から採用された新しいフォームクラスを使用する方法について書きます。

symfonyのアドミンジェネレータは非常に優れていますね。私は初めて使用したときは、単なるCRUDをやってくれるだけなのかな、とバカにしていたのですが、設定ファイルであるgenerator.ymlを編集したり、パーシャルを使用したりすることによって、とても柔軟に仕組みを作ることができるようになっています。しかも、コードをあまり書くことなくに、です。いやぁ、素晴らすぃ。

さて、今回の仕組みの目的は、sf_guard_userでは、格納できる情報が少ないので、sf_guard_user_profileを使用して格納できる情報を増やして、かつ、アドミンジェネレータで一つのモジュールで管理することです。

アドミンジェネレータでは、一つのモデルに対して一つのモジュールを管理する際には、とても有効に使うことができるのですが、複数のモデルを一つのモジュールで編集するには、苦労します。というか、そもそも、複数のモデルを一つのページで編集をさせるという設計がイマイチな感じがしますが、このsf_guard_user_profileを使用する以上は、しょうがないです。ということで、ゴリ押しです。

さて、プロジェクトやアプリケーションを作ったりするのは、ここでは説明しません。それらで躓いている人は、マニュアルを読んでください。もしくは20万円くらいで私が教えます。backendというアプリケーションがすでにあるということで、話を進めていきます。

さきほどのsf_guard_user_profileがある状態で propel:build-allをすると、lib/model/以下にsfGuardUserProfile(Peer|).php等のモデルクラスのファイルができていると思います。それを確認して、アドミンジェネレータを使用してみましょう。確認ですが、実際にアドミンジェネレーターで使用するモデルは、sfGuardUserProfileで、sfGuardUserではありません。

  1. $ ls
  2. apps/   config/  doc/  log/      symfony*  web/
  3. cache/  data/    lib/  plugins/  test/
  4.  
  5. $ ./symfony propel:init-admin backend user sfGuardUserProfile
  6. >> dir+      /home/shin/project/test/apps/backend/modules/user/config
  7. >> file+     /home/shin/project/test/apps/ba...dules/user/config/generator.yml
  8. >> dir+      /home/shin/project/test/apps/backend/modules/user/actions
  9. >> file+     /home/shin/project/test/apps/ba.../user/actions/actions.class.php
  10. >> tokens    /home/shin/project/test/apps/ba...dules/user/config/generator.yml
  11. >> tokens    /home/shin/project/test/apps/ba.../user/actions/actions.class.php

とすると、userモジュールが生成されて、デフォルトのCRUDができるようになりますね。画面のキャプチャを取ろうと思いましたが、面倒なので、想像で補ってください。生成されたgenerator.ymlは、次のようになっていますね。

  1. generator:
  2.   class:              sfPropelAdminGenerator
  3.   param:
  4.     model_class:      sfGuardUserProfile
  5.     theme:            default

そして、このgenerator.ymlをどんどん編集していきましょう。編集の仕方は、この辺を参照してください。

さて、アドミンジェネレータで生成された編集画面ですが、Userやら、Nameやら、Birthdayやら、Created atやら、Updated atの項目がありますが、このままでは、使えないです。というわけで、次のことをするとしましょう。

  1. ラベルを日本語化する
  2. created_at, updated_at, user_idとかがいらないので消す
  3. その代わり、sf_guard_userのusername, passowrd, is_activeを編集できるようする

ラベルを日本語化する

  1. generator:
  2.   class:              sfPropelAdminGenerator
  3.   param:
  4.     model_class:      sfGuardUserProfile
  5.     theme:            default
  6.  
  7.     fields:
  8.       name: { name: 名前 }
  9.       birthday: { name: 生年月日 }

fieldsの項目が増えただけです。編集可能なフィールドは、nameとbirthdayだけにしましょう。というわけで、その二つだけラベルをセットしました。

created_at, updated_at, user_idとかいらないので消す

  1. generator:
  2.   class:              sfPropelAdminGenerator
  3.   param:
  4.     model_class:      sfGuardUserProfile
  5.     theme:            default
  6.  
  7.     fields:
  8.       name: { name: 名前 }
  9.       birthday: { name: 生年月日 }
  10.  
  11.     list:
  12.       title: ユーザ一覧
  13.       display:
  14.         [ id, name ]
  15.       object_actions:
  16.         _edit: -
  17.         _delete: -
  18.  
  19.     edit:
  20.       title: ユーザ編集
  21.       display:
  22.         "基本情報": [ name ]
  23.         "詳細情報": [ birthday ]

とりあえず、sfGuardUserProfileで編集可能なフィールドは、nameとbirthdayだけですので、シンプルですね。

sf_guard_userのusername, passowrd, is_activeを編集できるようする

ユーザの情報では、usernameとpasswordを編集したいですよね?ついでに、is_activeも編集できるようにしましょう。つまり、有効ユーザか否かという項目です。さらにusernameは、メールアドレスの格納場所としましょう。本当はメールアドレスはemailとかmail_address等のフィールドとしたいところですが、しょうがないので、usernameをメールアドレスとして扱います。

まず、generator.ymlを修正してみます。

  1. generator:
  2.   class:              sfPropelAdminGenerator
  3.   param:
  4.     model_class:      sfGuardUserProfile
  5.     theme:            default
  6.  
  7.     fields:
  8.       email_address: { name: メールアドレス }
  9.       password: { name: パスワード }
  10.       name: { name: 名前 }
  11.       birthday: { name: 生年月日 }
  12.       is_active: { name: 有効ユーザ }
  13.  
  14.     list:
  15.       title: ユーザ一覧
  16.       display:
  17.         [ id, email_address, name, is_active ]
  18.       object_actions:
  19.         _edit: -
  20.         _delete: -
  21.  
  22.     edit:
  23.       title: ユーザ編集
  24.       display:
  25.         "基本情報": [ email_address, password, name ]
  26.         "詳細情報": [ birthday, is_active ]

fieldsのemail_address, password, is_activeにラベルを足しました。listの表示にemail_address, is_activeを足しました。editの編集表示にemail_address, is_activeを足しました。

さて、このままではエラーが出てしまいます。つまり、email_address, password, is_activeを持ってこれないのですね。私もここで少しはまってしまったのですが、アドミンジェネレータの生成するキャッシュファイルを見て、sfGuardUserProfileモデルのクラスに、usernameやpassword、is_activeのアクセサを作成すればいいことがわかりました。
中で、object_input_tagなどを使用しており、そこでゲッターメソッドを呼んでいましたので、実装します。

さらに、sfGuardUserProfileのインスタンスを保存する際に、ついでに、それに紐付けられたsfGuardUserのインスタンスも変更されたusername, password, is_activeを持って編集すればいいのだと理解しました。そして、それをsfGuardUserProfileモデルのクラスに実装します。

usernameは、メールアドレスとして使用するため、ちょっとだけかぶせてあります。

  1. class sfGuardUserProfile extends BasesfGuardUserProfile
  2. {
  3.     private $guardUser = null;
  4.  
  5.     public function getEmailAddress()
  6.     {
  7.         return $this->getUsername();
  8.     }
  9.  
  10.     public function setEmailAddress($value)
  11.     {
  12.         $this->setUsername($value);
  13.     }
  14.  
  15.     public function getIsActive()
  16.     {
  17.         return $this->getGuardUser()->getIsActive();
  18.     }
  19.  
  20.     public function setIsActive($value)
  21.     {
  22.         $this->getGuardUser()->setIsActive($value);
  23.     }
  24.  
  25.     public function getPassword()
  26.     {
  27.         return '';
  28.     }
  29.  
  30.     public function setPassword($value)
  31.     {
  32.         $this->getGuardUser()->setPassword($value);
  33.     }
  34.  
  35.     public function save($con = null)
  36.     {
  37.         if (is_null($con)) {
  38.             $con = Propel::getConnection();
  39.         }
  40.         try {
  41.             $con->begin();
  42.             $this->getGuardUser()->save($con);
  43.             $this->setUserId($this->getGuardUser()->getId());
  44.             parent::save($con);
  45.             $con->commit();
  46.         } catch (Exception $e) {
  47.             $con->rollback();
  48.             throw $e;
  49.         }
  50.     }
  51.  
  52.     private function getUsername()
  53.     {
  54.         return $this->getGuardUser()->getUsername();
  55.     }
  56.  
  57.     private function setUsername($value)
  58.     {
  59.         $this->getGuardUser()->setUsername($value);
  60.     }
  61.  
  62.     private function getGuardUser()
  63.     {
  64.         if ($this->guardUser) {
  65.             return $this->guardUser;
  66.         }
  67.         $this->guardUser = $this->getSfGuardUser();
  68.  
  69.         if (is_null($this->guardUser)) {
  70.             $this->guardUser = new sfGuardUser();
  71.         }
  72.  
  73.         return $this->guardUser;
  74.     }
  75. }

getPasswordが空文字列を返していますが、それはsfGuardPluginを使用するとデフォルトでは、復元できない形になってしまってしまうからです。管理者がパスワードが見れないという点ではいいのですが、どうするかは悩ましいところですね。平文で入れる方法もあるのですが、少々トリッキーなので、また別の機会に取り上げます。かもしれません。

はい。少々長いコードですね。最初の目的のコードを書かないようにするという目的からは少し反していますが、自分で組んだらもっと書かないといけないので、許してください。

これで、エラーがなく、編集が可能になりました。しかし、今度はメールアドレス、パスワード、有効ユーザの項目がdisabledになってしまっています。symfonyの中を見ると、sfCrudGenerator.class.phpで、CreoleTypesで見ているようです。そこで、disabledにされてしまっているので、簡単にはできなさそうでした。ということで、パーシャルの使用でゴリ押しします。userモジュールのactions, configなどのディレクトリのある階層にtemplatesディレクトリを作成して、_email_address.php, _password.php, _is_active.phpを作成します。
そして、それぞれのファイルに次のように書きます。
_email_address.php

  1. <?php
  2. $value = object_input_tag($sf_guard_user_profile, 'getEmailAddress', array (
  3.     'size' => 64,
  4.     'control_name' => 'sf_guard_user_profile[email_address]',
  5. ));
  6. echo $value ? $value : '&nbsp;';
  7. ?>

_password.php

  1. <?php
  2. $value = object_input_tag($sf_guard_user_profile, 'getPassword', array (
  3.     'size' => 64,
  4.     'control_name' => 'sf_guard_user_profile[password]',
  5. ));
  6. echo $value ? $value : '&nbsp;';
  7. ?>

_is_active.php

  1. $value = select_tag('sf_guard_user_profile[is_active]',
  2.     options_for_select(array(
  3.     '1' => '有効ユーザ',
  4.     '0' => '無効ユーザ'
  5.     ), (int)$sf_guard_user_profile->getIsActive())
  6. );
  7. echo $value ? $value : '&nbsp;';
  8. ?>

そして、generator.ymlでのeditの項目を、今作成したパーシャルで置き換えます。最終的に generator.ymlは以下のようになります。

  1. generator:
  2.   class:              sfPropelAdminGenerator
  3.   param:
  4.     model_class:      sfGuardUserProfile
  5.     theme:            default
  6.  
  7.     fields:
  8.       email_address: { name: メールアドレス }
  9.       password: { name: パスワード }
  10.       name: { name: 名前 }
  11.       birthday: { name: 生年月日 }
  12.       is_active: { name: 有効ユーザ }
  13.  
  14.     list:
  15.       title: ユーザ一覧
  16.       display:
  17.         [ id, email_address, name, is_active ]
  18.       object_actions:
  19.         _edit: -
  20.         _delete: -
  21.  
  22.     edit:
  23.       title: ユーザ編集
  24.       display:
  25.         "基本情報": [ _email_address, _password, name ]
  26.         "詳細情報": [ birthday, _is_active ]

これで、とりあえず完了です。sfGuardUserProfileを保存すると、sfGuardUserもそのトランザクション中で保存されます。もう一ひねりしたいところは、パスワードの編集なのですが、これは、作成する仕組みが管理者がパスワードが見えるべきか否か、といった議論になってしまいますので、それぞれのアプリで応用してみてください。

また、sfGuardPluginの方に付いてくるモジュールでは、permissionやgroup roleに関しても登録できるようになっていますが、力尽きましたので、いつか書くかもしれません。ここで書いたように基本は、同じです。かぶせて保存です。

さて、全然関係ないですが、symfonyのコーディングスタンダードには、ちょっと好きになれません。生成されるソースコードがスペース2つだったり、if文やwhile文にも中括弧が次の行にあったりすると、嫌で嫌でしょうがないです。クラスやメソッドの始まりなら次の行から中括弧があっていいんですけどね。

しかし、パート2まで書けるかな。疲れてきた。次の方がややこいし。

パーシャルでゴリ押し?するあたりとても参考になりました。
ありがとうございました。

ところでコーディング規約気持ち悪いですよね。
クラス名もUpperCamelCaseって書いてあるのにsf~だったり。
スペース二つは普通に見づらいと思います。

中括弧のスタイルは私も大野さんと同じようなスタイルが好きなのですが
、symfony規約どおりにやってみたら以外に見やすいような気もします。

Comment by gomo — November 6, 2008

gomoさん

コメントどうもありがとうございます。
admin generatorは、1.2で一新されるようなので、期待ですね。

コーディング規約は確かにそうなんですよねぇ。でも、統一性を出さないといけないと思うのですが。。。私はPEARコーディング規約で育ったので、特にsymfonyの規約には抵抗がありますね。でも、そんなことは言ってられないので、次のプロジェクトからsymfonyに見習います。

Comment by shin — November 8, 2008

Leave a comment

Bloglines feedburner