A Django site.
9月 21, 2008
» symfony1.1でswiftを使ってみた。

最近の開発は、ずっとsymfonyなので心中するしかないかなー、なんて思っていたりします。しかし、実際に使っていると、フレームワークではカバーしきれないコードを、ゴリゴリと、かっこ悪い方法で実装してしまったりして、嫌悪感いっぱいになってしまうことがあります。

その一つがメール周りです。symfony1.1からsfMailを使わない方向になりました(まぁ、使おうと思えば使えるけど)。そこで、代替とされているのが、Swift Mailerですね。直接SMTPが叩くので、速いだとか負荷が減るだとか、ほげほげだということで、swiftが採用されているようですが、個人的な感想を言えば、私はsfMailerで十分間に合っていたと思います。。。

とも言っていられないので、ちょっとswiftを使ってみました。でも、
How to send emails in symfony 1.1The symfony Cookbook How to send an emailを読んでもなんかしっくり来ないんですよね。理由は、これらのハウツーには、メールの本文をコントローラに書く方法しか提供していないからだと思います。確か前のsfMailerではViewテンプレートにメールの本文が書けたのですが、それってswiftでどうやったらいいのかなー、なんて思って調べてやってみました。まぁ、Jonathan Wageさんのアイデアなんだけどね。

というわけで、彼のアイデアをまとめると次のような感じか。
メールを使うアクションクラスをすべてsfSwiftMailerActionsの子クラスとする。
実際に呼び出す際には、親クラスの実装メソッドsendMailにモジュール名とアクション名を渡して、そのテンプレートに本文を書く。

  1. <?php
  2. class sfSwiftMailerActions extends sfActions
  3. {
  4.   public function preExecute()
  5.   {
  6.     $mailVars = sfSwiftMailerVars::getInstance()->getAll();
  7.  
  8.     foreach ($mailVars as $key => $value)
  9.     {
  10.       $this->$key = $value;
  11.     }
  12.   }
  13.  
  14.   public function sendEmail($module, $action)
  15.   {
  16.     sfConfig::set('symfony.view.' . $module . '_' . $action . '_layout', false);
  17.     $body = sfContext::getInstance()->getController()->getPresentationFor($module, $action);
  18.  
  19.     $mailVars = sfSwiftMailerVars::getInstance();
  20.  
  21.     $message = $mailVars->has('message')
  22.       ? $mailVars->get('message')
  23.       : new Swift_Message(null, null, 'text/html');
  24.     $swift = $mailVars->has('swift')
  25.       ? $mailVars->get('swift')
  26.       : new Swift(new Swift_Connection_Sendmail(Swift_Connection_Sendmail::AUTO_DETECT));
  27.  
  28.     $message->setSubject($mailVars->get('subject'));
  29.     $message->setBody($body);
  30.  
  31.     $swift->send($message, $mailVars->get('recipients'), $mailVars->get('from'));
  32.     $swift->disconnect();
  33.  
  34.     $mailVars->clear();
  35.   }
  36.  
  37.   public function __set($key, $value)
  38.   {
  39.     sfSwiftMailerVars::getInstance()->set($key, $value);
  40.  
  41.     return parent::__set($key, $value);
  42.   }
  43.  
  44. }
  45.  
  46. class sfSwiftMailerVars extends sfParameterHolder
  47. {
  48.   static $instance = null;
  49.  
  50.   public static function getInstance()
  51.   {
  52.     if (!self::$instance)
  53.     {
  54.       self::$instance = new sfSwiftMailerVars();
  55.     }
  56.  
  57.     return self::$instance;
  58.   }
  59.  
  60. }

ふむ。なるほど。テンプレートの内容を取るだけななら

  1. $body = sfContext::getInstance()->getController()->getPresentationFor($module, $action);

でできてしまうのですね。

私が手元で実装したものでは、もう少し劣化させて、上記とは違って、SwiftクラスやSwift_Messageクラスは直書きでやってしまっています。まぁ、デフォルトの挙動なんていらないと思うので。というわけで、このソースはテストはしていません。ほぼ同じものを手元で書いて、その動作は確認しているので、だいたいのロジックはこれでいいようです。

あとは、実際のアクションクラスにこんな感じで書けばいいのですね。

  1. class hogeActions extends sfSwiftMailerActions
  2. {
  3.   public function executeEdit($request)
  4.   {    $swift->send($message, $mailVars->get('recipients'), $mailVars->get('from'));
  5.     $this->recipients = 'example@ganchiku.com';
  6.     $this->from = 'example@ganchiku.com';
  7.     $this->subject = 'hello world';
  8.     $this->sendEmail('hoge', 'confirm');
  9.   }
  10.  
  11.   public function executeConfirm($request)
  12.   {
  13.   }
  14. }

で、あとは、confirmSuccess.phpに本文を適当に書けばいいのですね。ふーむ。確かに、すっきりはしますが、結構面倒ですね。。。もっといい方法があったら教えてください。

8月 4, 2008
» symfony1.1でsfGuardPluginを使う。パート2

ここ二日通っているベトナム料理のレストランの眼鏡っ子ウェイトレスがかわいくて、通おうかなーと思ってる私は、いろんな人に助けれながらハワイ島のヒロに滞在しています。ぜんぜん関係ないですが、今日は相撲の土俵の屋根の解体作業を手伝いました。きつい肉体労働で疲れました。。。

さて、パート1では、sfGuardPluginのsfGuardUserProfileとsfGuardUserを1対1のテーブル関係でリレーションがあった際に、アドミンジェネレータで一つのモデルをCRUDしているかのように扱う方法を説明しました。

しかし、実はこのアドミンジェネレータの方法は、symfony1.1的には、フォームの使い方が古いんですね。symfony1.1からは、sfFormクラスを使ってフォームを作るように、方法が変わったのです。なので、form_tagとかのヘルパー関数を使うのではなく、sfFormクラスの拡張クラスを使うことになります。また、Propelを使用している際には、うまく同期をとってくれるsfFormPropelの拡張クラスを使用することになりますね。例によって、ジェネレータで、ベースクラスと実際の処理を書く空クラスが作られ、それを上書きしていくという方法で実装するようになります。

さて、前提条件をここでおさらいするのは面倒なので、前回のパート1のまま、進めてみます。つまり、schema.ymlにsf_guard_user_profileを用意して、build-allをした状態とします。パート1では、backendとしましたが、今回は、frontendとしてみます。

今回のフォーム作成においては、CRUDのCreationのフォームとその値のデータベースへの登録についてを説明します。

パート1と同じように1対1の関連を持つ二つのテーブルを同時に登録する際の使い方について書きます。モジュール名はなんでもいいのですが、ベタにuserとしておきます。

  1. $ ./symfony project:init-module frontend user
  2. PHP Warning:  Xdebug MUST be loaded as a Zend extension in Unknown on line 0
  3. >> dir+      /home/shin/project/test/apps/frontend/modules/user/templates
  4. >> file+     /home/shin/project/test/apps/fr...user/templates/indexSuccess.php
  5. >> dir+      /home/shin/project/test/apps/frontend/modules/user/actions
  6. >> file+     /home/shin/project/test/apps/fr.../user/actions/actions.class.php
  7. >> file+     /home/shin/project/test/test/fu...al/frontend/userActionsTest.php
  8. >> tokens    /home/shin/project/test/test/fu...al/frontend/userActionsTest.php
  9. >> tokens    /home/shin/project/test/apps/fr...user/templates/indexSuccess.php
  10. >> tokens    /home/shin/project/test/apps/fr.../user/actions/actions.class.php

さて、環境が整いました。ここでフォームの入力可能なフィールドを何とするか決めます。パート1との続きということで、メールアドレス、パスワード、名前、生年月日とします。また、パート1と同じくsf_guard_userのusernameは、メールアドレスを入れる項目とします。パート1では、propel:build-allなどで、モデルを作成した際に、ついでにフォームクラスの雛形も生成されます。lib/form/sfGuardUserProfileForm.class.phpがすでにあると思います。そのクラスsfGuardUserProfileFomの空のメソッドconfigureに、フォームで使用するフィールドを選択したり、そのフィールドのラベルを変更したり、バリデターをつけたりしましょう。

つまり、大きく分けて、次の3つを行います。

  1. フォームで使用するフィールドを選択する
  2. ラベルを日本語化する
  3. バリデーションを追加する

フォームで使用するフィールドを選択する

さきほど、どのフィールドを入力可能とするか、決めました。メールアドレス、パスワード、名前、生年月日ですね。では、さっそく、それをセットしましょう。

  1. public function configure()
  2.     {
  3.         $years = range(date('Y') - 60, date('Y') - 17);
  4.         $this->setWidgets(array(
  5.             'username' => new sfWidgetFormInput(),
  6.             'name' => new sfWidgetFormInput(),
  7.             'password' => new sfWidgetFormInputPassword(),
  8.             'birthday' => new sfWidgetFormDate(array('format' => '%year%年%month%月%day%日', 'years' => array_combine($years, $years)))
  9.         ));
  10.     }

誕生日の年の項目は、17歳から60歳の人を対象としてみます。それより若い人、老いた人を対象にしたい場合は、適当に修正してください。

そして、作成したモジュールuserのactions.class.phpにexecuteRegisterメソッドを追加して、Viewファイル、registerSuccess.phpもuserモジュールのtemplates以下に追加しましょう。
actions.class.php

  1. public function executeRegister($request)
  2.     {
  3.         $this->form = new sfGuardUserProfileForm();
  4.     }

registerSuccess.php

  1. <?php $user = $form->getObject() ?>
  2. <form action="<?php echo url_for('user/register') ?>" method="post">
  3.   <table>
  4.     <tfoot>
  5.       <tr>
  6.         <td colspan="2">
  7.           <input type="submit" value="登録する" />
  8.         </td>
  9.       </tr>
  10.     </tfoot>
  11.     <tbody>
  12.       <?php echo $form ?>
  13.     </tbody>
  14.   </table>
  15. </form>

これで、簡単な登録フォームができました。デザイナーの方とフォームのデザインなどを協調的に作業する際には、この$formをこうやって単にechoするのではなく、詳細に書いていくことができるようですが、面倒ですので、ここではしません。

ラベルを日本語化する

さて、パート1のアドミンジェネレータのときと同じく、ラベルがまだ英語になっていますので、日本語に書き換えましょう。sfGuardUserProfileFormクラスのconfigureメソッドの先ほど書いた後あたりに、に次の行を加えて、ラベルをセットします。

  1. $this->widgetSchema->setLabels(array(
  2.             'username' => 'メールアドレス',
  3.             'password' => 'パスワード',
  4.             'name' => '名前',
  5.             'birthday' => '生年月日'
  6.          ));

バリデーションを追加する

これで、フォームができました。次は、バリデーションです。メールアドレスの項目は、必須で、メールアドレスのフォーマットチェック、ユニークチェックをしましょう。パスワードは、必須項目とするだけにします。nameも必須項目とするだけにします。実際に使用する際には、文字数チェックなども、そのアプリの仕様に基づいて変更してください。そして、ラベルをセットした後あたりに、次の行を加えて、バリデーションをセットします。

  1. $this->setValidators(array(
  2.             'username' => new sfValidatorEmail(
  3.                 array(),
  4.                 array('required' => 'メールアドレスの項目は必須です。',
  5.                       'invalid' => 'メールアドレスのフォーマットが間違っています
  6. 。もう一度ご確認ください。')),
  7.             'nickname' => new sfValidatorString(
  8.                 array(),
  9.                 array('required' => 'ニックネームの項目は必須です。')),
  10.             'password' => new sfValidatorString(
  11.                 array(),
  12.                 array('required' => 'パスワードの項目は必須です。')),
  13.             'birthday' => new sfValidatorDate(array('required' => false)),
  14.         ));
  15.         $this->validatorSchema->setPostValidator(new sfValidatorPropelUnique(
  16.             array('model' => 'sfGuardUser', 'column' => array('username')),
  17.             array('invalid' => '指定のメールアドレスは既に登録されています。')
  18.         ));
  19.  
  20.         $this->widgetSchema->setNameFormat('user[%s]');

最後のsetNameFormatは、パラメータのネームスペースのようなものです。フォームの値がuserというキーの連想配列に入るようにしています。つまり、user[password]とか、user[birthday][year]とかになって、サーバにパラメータが送られてくるようになります。

さて、あとは、actionクラスを修正して、ちゃんとバリデーションが通った際には、保存できるようにしましょう。さきほどは、formという変数をレンダーしているだけでしたので、ロジックを書きます。

  1. public function executeRegister($request)
  2.     {
  3.         $this->form = new sfGuardUserProfileForm();
  4.         if ($request->isMethod('post')) {
  5.             $this->form->bind($request->getParameter('user'));
  6.             if ($this->form->isValid()) {
  7.                 $this->form->setIsActive(false);
  8.                 $user = $this->form->save();
  9.                 $email = $user->getEmailaddress();
  10.                 // ここで確認メールを送って、is_activeをtrueにする操作をしたり。
  11.             }
  12.             // validation failed
  13.         }
  14.     }

ところで、

  1. $this-form->setActive(false);

とあるのですが、それは、まだ実装していませんね。なぜこんなことをするかというと、デフォルトのsfGuardUserのis_activeの値は、trueなのです。なので、セットせずに、保存してしまうとここで登録したユーザがそのままログインができるようになってしまうのですね。確認メールを送信するなどして、ちゃんと有効なメールアドレスを登録しているユーザだけを有効にしたい場合が多くあると思いますので、sfGuardUserProfileFormクラスにsetIsActiveメソッドを追加しておきましょう。確認メールの送り方などは、力尽きましたので、ここでは書きません。

というわけで、sfGuardUserProfileFormクラスは最終的に以下のようになりました。

  1. class sfGuardUserProfileForm extends BasesfGuardUserProfileForm
  2. {
  3.     public function configure()
  4.     {
  5.         $years = range(date('Y') - 60, date('Y') - 17);
  6.         $this->setWidgets(array(
  7.             'username' => new sfWidgetFormInput(),
  8.             'name' => new sfWidgetFormInput(),
  9.             'password' => new sfWidgetFormInputPassword(),
  10.             'birthday' => new sfWidgetFormDate(array('format' => '%year%年%month%月%day%日', 'years' => array_combine($years, $years)))
  11.         ));
  12.  
  13.         $this->widgetSchema->setLabels(array(
  14.             'username' => 'メールアドレス',
  15.             'password' => 'パスワード',
  16.             'name' => '名前',
  17.             'birthday' => '生年月日'
  18.         ));
  19.  
  20.         $this->setValidators(array(
  21.             'username' => new sfValidatorEmail(
  22.                 array(),
  23.                 array('required' => 'メールアドレスの項目は必須です。',
  24.                       'invalid' => 'メールアドレスのフォーマットが間違っています。もう一度ご確認ください。')),
  25.             'name' => new sfValidatorString(
  26.                 array(),
  27.                 array('required' => '名前の項目は必須です。')),
  28.             'password' => new sfValidatorString(
  29.                 array(),
  30.                 array('required' => 'パスワードの項目は必須です。')),
  31.             'birthday' => new sfValidatorDate(array('required' => false)),
  32.         ));
  33.  
  34.         $this->validatorSchema->setPostValidator(new sfValidatorPropelUnique(
  35.             array('model' => 'sfGuardUser', 'column' => array('username')),
  36.             array('invalid' => '指定のメールアドレスは既に登録されています。')
  37.         ));
  38.  
  39.         $this->widgetSchema->setNameFormat('user[%s]');
  40.     }
  41.  
  42.     public function setIsActive($value = false)
  43.     {
  44.         $this->object->setIsActive($value);
  45.     }
  46. }

もう一息です。toArrayとfromArrayを上書きする必要があるのを忘れていました。
sfGuardUserProfile.phpにtoArrayとfromArrayを上書きしてみます。

  1. public function fromArray($arr, $keyType = BasePeer::TYPE_PHPNAME)
  2.     {
  3.         parent::fromArray($arr, $keyType);
  4.         $this->setUsername($arr['username']);
  5.         $this->setPassword($arr['password']);
  6.     }
  7.  
  8.     public function toArray($keyType = BasePeer::TYPE_PHPNAME)
  9.     {
  10.         $result = parent::toArray($keyType);
  11.         $result['username'] = $this->getUsername();
  12.         $result['password'] = $this->getPassword();
  13.         return $result;
  14.     }


これで、とりあえず完了です。パート1で使用したアクセサやsaveメソッドは、そのまま使用することで、sf_guard_userとsf_guard_user_profileという二つのテーブルに保存することができるようになります。

ふー。疲れました。

間違いがありましたら、いろいろ教えてください。

7月 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まで書けるかな。疲れてきた。次の方がややこいし。

2月 20, 2008
» symfonyで全文検索でもしてみるか。

WEB+DB Pressを今回の外こもり合宿のお供に連れていった際に、ニコ動の記事が載っていたので勉強をさせてもらった。特に、負荷対策について勉強になったな。私は、今まで作ってきた仕組みはそこまで負荷対策に敏感にならずに開発してきたので、私の管轄外だよなー、なんて思っていたのだけども、そんなことをいつまでも言ってられない状態なので調べてやってみた。つーか、負荷対策が必要なくらいなシステムに従事してみたい。。。ウレシイ悲鳴ってやつか。

ニコ動でも使用しているように、Senna+MySQL(tritonn)を採用してみた。面倒だなと思ったのは、次のところ。
1:Senna+MySQL(tritonn)では、MyISAMを指定しなければいけない。他は、InnoDBを採用するのに。
2:検索結果をページングしたいときは、sfAdvancedPropelPagerを採用しなければいけない。
3:summaryテーブルを作成した方が良い。

1:Senna+MySQL(tritonn)では、MyISAMを指定しなければいけない。他は、InnoDBを採用するのに。

Senna+MySQL(tritonn)は、MyISAMのみの対応なのだけども、MyISAMは更新系に弱いそうで、ニコ動でも検索以外のところでは、InnoDBを採用しているようだった。これの何が面倒かと言うと、symfony propel-build-allとかで普通にモデルを作成したり、テーブルを作成すると、テーブルごとにMyISAMだとかInnoDBって変更することができないのね。たぶん。なので、一度、InnoDBで作成した後で、MyISAMに作りなおしてあげる必要があるのだよ。propel-build-allとかにhookをかけてあげればいいのだろうけど、やり方がわからなかったので、単純にSQLを流し込むことにした。

  1. DROP TABLE IF EXISTS 'summaries';
  2. CREATE TABLE summaries
  3. (
  4.  'id' integer NOT NULL AUTO_INCREMENT,
  5.  'hoge_id' integer NOT NULL,
  6.  'name' varchar(255),
  7.  'descritption' text,
  8.  'more_text' text,
  9.  'more_more_text' text,
  10.  PRIMARY KEY ('id'),
  11.   FULLTEXT INDEX fulltext_index USING SECTIONALIZE (name, description, more_text, more_more_text)               
  12. )Type=MyISAM DEFAULT CHARSET = utf8;

とかで上書きしてあげる。この時点で面倒すぐる。で、SECTIONALIZEを指定することにより、複数のカラムを全文検索の対象にすることができるのだ。実際検索する際に、重みつけなんかをすればよし。

前後するけど。。。

3:summaryテーブルを作成した方が良い。

実際このテーブルはサマリーテーブルなのだけども、検索要素をテーブルをまたがって検索するのではなく、バッチ処理なんかで走らせたり、更新が起きたら走らせるなど方法で、サマリーテーブルを更新する必要がある。ニコ動は同期性をよりも、バッチ処理で速い検索をポリシーとして置いているので、このように対応したそうだ。また、askeetを読むと、更新時に更新したQuestionに関するindexを毎回更新しているみたいだけど、この辺もポリシーかな。

私もいろいろ考えたが、確かに、検索結果の反映に関しては、そこまで即時性は持つ必要がないと思うので、ニコ動の考えに賛成だな。それにJOINとかしているとやっぱり検索が重くなるそうなので、極力シンプルなSQLを発行する必要があるみたい。他のは、キャッシュでなんとかすればいいとは思うけど、検索結果をキャッシュはしないよね。

2:検索結果をページングしたいときは、sfAdvancedPropelPagerを採用しなければいけない。

まぁ、sfAdvancedProplePagerではなくてもいいのだけど、普通のsfPropelPagerではCriteriaを渡すことが無理なので、ごめんなさい。ここ訂正します。SQLからCriteriaを作成することができないので、sfPropelpagerにsetCriteriaで渡せないのです。独自の方法でSQLを渡す方法を考える必要がある。がんばったらそれ専用のCriteriaを作ることができそうな感じもするが、ここではsfAdvancedPropelPagerを採用した方が解決方法としていいと思う。

sfAdvancedPropelPagerは、Code Snippetsとして提供されている。http://www.symfony-project.org/snippets/snippet/119
しかし、こんなことが書いてある。

PLEASE NOTE: You will have to change all private methods and properties in sfPropelPager from 'private' to 'protected'. This won't break paging elsewhere in your projects, I promise you! What it will do is allow this subclass to gain access to it's parent's data and functionality. It should be noted that this addon was developed with the current stable version (0.6.3) of Symfony in mind.

sfPropelPagerを継承しているので、sfPropelPagerのprivateメソッドやプロパティをprotectedに書き換えないといけない、と書いてあった。しかしながら、今開発環境で使用しているsymfonyは、1.0.8なのだけども、protectedになっていたので、書き換える必要はない。0.6.3の場合かな。

というわけで、sfAdvancedPropelPager.class.phpとかに適当に名前を書いて、pathが通るところに置いてあげる。この置く場所が悩ましいんだよなぁ。。。私は、/lib/の下に置いてしまったけど、本当はどこに置くべきなの?

さて、使いかたは、次のような感じ。検索するHogePerrクラスとかにsearchとか言うメソッドでも置いてみて、そこで、sfAdvancedPropelPagerを使用するだけ。

  1. public static function search($word, $page = 1, $limit = 20)
  2.     {
  3.         $con = Propel::getConnection();
  4.         $query = '
  5.             SELECT * FROM summaries WHERE
  6.             MATCH (name, description, more_text, more_more_text)
  7.             AGAINST (? IN BOOLEAN MODE)
  8.             ';
  9.         $stmt = $con->prepareStatement($query);
  10.         $priority = sprintf("W1:%s,2:%s,3:%s,4:%s",
  11.             sfConfig::get('app_search_name'),
  12.             sfConfig::get('app_search_description'),
  13.             sfConfig::get('app_search_more_text'),
  14.             sfConfig::get('app_search_more_more_text')
  15.             );
  16.         $stmt->setString(1, $priority . " " . $word);
  17.         $pager = new sfAdvancedPropelPager('Summary', $limit);
  18.         $pager->setStatement($stmt);
  19.         $pager->setPage($page);
  20.         $pager->init();
  21.         return $pager;
  22.     }

$priorityは、検索のスコアリングね。あとあといじりたいときのために、こうやってapp.ymlとかに書いておいて、調整をするといいかも。

templateの方では、sfPropelPagerと使いかたは同じなので、言及はしない。次のページとかへのリンクを@search?q=クエリー文字列&p= $pager->getNextpage()とかにするだけ。

ちょっと凝ったことをするとやっぱりフレームワークって面倒やね。単純なCRUDだけの仕組みならいいのだけど、世の中で求められているものはもう少し複雑なんだよね。prototype.js以外のJavaScriptフレームワークをフレームワークで使用しようとすると、ヘルパー関数がなかったりして、採用しにくいし。sfCSRFPluginとかAjaxでcsrf attack detectedとか怒ってくるので、泥臭いことしないといけないし。

私は今まで全文検索使ったことがなかったが、ようやく全文検索を使う機会ができました。HE使っていなくて、師匠に顔向けができないけど。。。

12月 12, 2007
» substrがfalseを返すとき

先日PHPUnit関係で、バグに関して見てくれとのことでメールがありました。
Selenium関係のところで動作がおかしいとのことでしたので、以下のtracに登場してみました。

InvalidArgumentException when running Selenium test

なるほど。問題はsubstrがfalseを返すときに起こるものですね。てっきりstringしか返さないものだと思ったら、

string substr ( string $string , int $start [, int $length ] )

確かにこう書いてあります。

length が指定され、かつ正である場合、 返される文字列は start (string の長さに依存します) から数えてlength 文字数分となります。 もし string が start の文字列長より小さいもしくは等しい場合、FALSE が返されます。

PHP: substr - Manual

より小さい場合はわかるけど、$stringと同じ長さのときもfalseを返してくれるのがなんとも。そして、このバグは、その同じ長さのときに空文字列ではなく、falseを返すので、InvalidArgumentExceptionを投げてしまうというものでした。マニュアルをちゃんと読まないとダメでしたね。勉強になりました。そしてこれからは関数を使う以上、ちゃんと理解してから使った方がいいと思いました。

そして、今日は日比谷図書館。でも、雰囲気は千代田図書館の方がいいですね。キレイやし、夜10時までやってるのもポイント高いです。近くにSUBWAYがあるのもいいですね。明日からは千代田図書館に戻ります。

11月 24, 2007
» 三連休はテスト漬け

三連休はテストですYO!
実は、今回が初めてのsymfony採用なのだけども、少しずつだけどわかってきた。askeetは実際にやってみる必要はないかもしれないが、どうやってsymfonyでリファクタリングをしていったらいいかが、わかるので参照するのに重宝している。ちょっと古いのが難点だけど、根本となる考えは変わらないので勉強になるね。

そして、最近作っていたところが汚くなってきたので、リファクタリング。そして、テスト。そしてリファクタリング。作業実績自体はあがらないけど、これが大事やね。

ユニットテストで使用しているプラグインは、sfModelTestPlugin。機能はこんな感じ。

* Uses a separate test database
* Automatically reloads test data before each group of tests
* Easy to install and use
* Support for Propel 1.2, Propel 1.3, and Doctrine

これで、テスト用のDBにデータを突っ込んで、実際にモデルクラスのメソッドが動くかどうかのテストができる。私的には、Criteriaクラスのオブジェクトをコントローラに書いたら負けだと思っているので、どんどんモデルに出す。そして、テスト。

このプラグインなんだけど、なぜかsymfonyコマンドでインストールすることはできなかった。しょうがないので、tarボールを展開して対応。Installの項目そう書いてあるのでね。

私はプロダクトにプラグインを採用することに関しては、ビンゴでなければ少し抵抗があるのだけど、この手のツールは積極的に採用したい。つーか、symfonyのコアライブラリで実装してもらいたい。

なんかもうじきsymfony1.1が出そうやね。私が今使っているのが1.0.8なので、sfCSRFPluginを入れているけど、1.1からはコアの方に組み込まれるみたい。結構大幅に機能が追加されそうな感じなので、ちゃんとチェックしないといけないなぁ。

というわけで、現段階でできるユニットテストは一応揃ったので、次は機能テストの方をやりまーす。

11月 4, 2007
» symfonyにctagsを付けてみた。

ctagsって便利よねー。

内部で例外投げているときとか結局ライブラリのソース辿るときが往々にしてあるので、symfonyさんをさらに便利にするためにctagsを付けてみた。

最初は、私の環境のctagsのバージョンが5.4だったためか、親クラスのメソッド持ってきてくれなかったので、「つかえねぇ!」と思っていたけど、バージョンを5.7にしたらちゃんと親クラスのメソッドを持ってきてくれた!ヤッター!おそらく

ctags-5.7 (04 Sep 2007)
..snip
* Added support for interfaces and static/public/protected/private functions (PHP).

辺りかなぁ。まぁ、どちらにせよ、これで、Ctrl-] で移動、Ctrl-tで戻ってこれるぜ。つーか、これ超便利!

  1. ctags -f symfony.tags --langmap=PHP:.php --php-types=c+f+d -R `find $HOME/PEAR/lib/php/symfony -name '*.php'`

私は、HOME以下にPEARをインストールしているので、symfonyが入っているのは、$HOME/PEAR/lib/php/symfonyなので、それは適当にほげほげしてください。

で後は、vimrcに

  1. autocmd filetype php :set tags+=$HOME/var/vim/tags/symfony.tags

とか書いておくだけ。たのすぃー。あー。私のsymfony.tagsの置き場所も適当にほげほげしてくださいな。
(more...)

» 季節を外した!

今はタオ島(Koh Tao)。タオ島に行くためには、飛行機でサムイ島(Koh Samui)に行かないといけないのだが、サムイ島は半日で嫌になったので速攻脱出してタオ島へ。私はこっちの方が好き。さて、現在は約10ヶ月前にポストしたこういう働き方っていいかもね。が実際にできるかどうかを模索中である。

今回タオ島を選んだのは、前回アンダマン海の方には行ったのだけど、Gulf of Thailand(タイ湾)の方には来なかったので、ぜひ来てみたいと思っていたからだ。それにタイならだいたいどんな感じかイメージが付くので、それほど無謀ではないと思っていたからだ。

つーか、今タイの南部は雨期だった。。。もうちょっと調べればよかったな。一日中雨というわけではないからいいけどさ。今日は、ずっと晴れてたし。まぁ、雨の方が籠って開発できるというもんだ、とポジティブにも考えてみるテスト。それに、人が大杉でないのが良い。さらにバーゲンできるのも良い。ものは考え用さ。タオ島は、ダイビングの島だけあってハイシーズンは大変なことになるらしいから、ローシーズンの今だからこそいいこともいっぱいあるさ。

で、実際の仕事だが、小さなトラブルがありながらも、なんとか開発が進んでいる。すでにプロトタイプがあるので、外部仕様的にもだいぶ楽に開発を進めていくことができる。とりあえず、帰るまでにプロトタイプと6割の機能をカバーする予定。もちろん拡張しやすいようにね。

さて、いきなりバンコクからサムイ島への飛行機が4時間遅れたので、空港でユーザ管理機能を5割近く開発。つーか、symfony楽だね。validatorやfillinをうまいこと使うと本当に楽。sfPropelUniqueValidatorとかsfCompareValidatorとか、今まで自前で作っていたのをやってくれるのは本当にうれしい。つーか、楽すぎ。せっかくフレームワークを使うのだから、フレームワークの持っている機能をフルに使って行くぜ。そうでなければ、フレームワーク使う意味ないと思うしね。

ちょっと技術メモを残しておくなら、次のことか。

  • symfony propel-build-modelで、TIMESTAMPのデフォルト値を指定しているときに、symfony propel-build-model fails: Unable to parse default value as date/time valueとかいうので、created_atとかupdated_atは、schemaに余分なものを書かないようにした。勝手に制約