Displaying posts written in

5月 2010

find(‘list’) で集約関数を使用する方法

単に field パラメータに指定するだけではうまく動作しないので、一旦 find(‘all’) で結果を取得してから Set::combine を使って同等の構造を得る。

// user_id => 件数
$tmp = $Post->find('all',
                   array('fields' => array('user_id', 'COUNT(*) AS posts_count'),
                         'group'  => 'user_id'));
return Set::combine($tmp, '{n}.Post.user_id', '{n}.0.posts_count');

Set::combine は恐ろしく多機能なので、find(‘list’) をそのまま使うよりずっと多くの事ができる。例えば次のように、複数カラムで GROUP BY した結果をフラットな配列にまとめることができる。

// "user_id:category_id" => 件数
$tmp = $Post->find('all',
                   array('fields' => array('user_id', 'category_id', 'COUNT(*) AS posts_count'),
                         'group'  => array('user_id', 'category_id')));
return Set::combine($tmp,
                    array('{0}:{1}', '{n}.Post.user_id', '{n}.Post.category_id'),
                    '{n}.0.posts_count');

値を連想配列にする事もできる。

// user_id => array("created_max" => 最大値, "created_min" => 最小値)
$tmp = $Post->find('all',
                   array('fields' => array('user_id',
                                           'MAX(created) AS created_max',
                                           'MIN(created) AS created_min'),
                         'group'  => 'user_id'));
return Set::combine($tmp, '{n}.Post.user_id', '{n}.0');

jQuery History is now on Github

I created a fork of jQuery History plugin on Github.

Though this is just a fork for now, Mikage Sawatari, the original author of this plugin, and I agreed that it could be the mainstream in the near future.

I’ve already done some major changes to the script structure:

  • It no longer uses the Webkit-specific hack for updating the location object, because recent versions of Webkit can correctly update the object just like Firefix and IE8.
  • Hashes are percent-encoded with encodeURIComponent before being put to location.hash. So you can use any characters such as `?’ in your hash.
  • history.load does not execute the callback unless hash is changed.
  • Updated API/code format. Thanks to Kevin Dalman.

It works on IE6, IE7, IE8, Firefox3, Safari4, Chrome4, and Chrome5.

If you have any problems or feature requests, please create a new issue in the ITS. And of course, you can create your own fork.


jQuery Historyプラグインは今後Githubで管理されることになりました。

Mikage Sawatariさんによる元々のバージョンに対し、次のような大きな変更が加えられています。

  • Webkitで動かすためのハックは不要になったので取り除かれました。
  • location.hash に代入する前に encodeURIComponent によるパーセントエンコーディングを行うようにしました。これによって ? を含むあらゆる文字を hash として保存できるようになりました。
  • ハッシュが変更されていない場合はコールバックが実行されないようになりました。
  • API/コーディングスタイルを変更しました。この変更はフォーラムのKevin Dalman氏の投稿に依ります。

IE6, IE7, IE8, Firefox3, Safari4, Chrome4, Chrome5で動作します。

Githubのバグトラッキングシステムに登録してください。もちろん自分でforkを作って修正していただいても構いません。

メールの送信をテストする

環境: CakePHP1.3

ComponentTestCaseを使ってメールの送信をテストする方法をまとめておきます。Qdmailerを使うことを前提としていますが、標準のEmailComponentなど他のコンポーネントでも同じようにテストできると思います。

単純なケース

メール送信をテストする上で最も重要なことは、メールの種類ごとにコンポーネントを作るということです。

まずメール送信コンポーネントに共通の振る舞いを定義するため、以下のような Mailer クラスを app/libs に作ります。 実際のインターフェイスや初期化処理などは自分のアプリケーションに合わせて調整してください。

abstract class Mailer extends Object
{   
  var $components = array('Qdmailer.Qdmailer');
  var $_controller;
  var $_settings;
 
  function initialize($controller, $settings=array()) {
    $this->_controller = $controller;
    $this->_settings = $settings;
  }
 
  function create() {
  }
 
  protected function _rawSend() {
    $this->Qdmailer->send();
  }
 
  function send() {
    $this->Qdmailer->resetHeaderBody();
    $args = func_get_args();
    $this->dispatchMethod('create', $args);
    $this->_rawSend();
  }
}

次にメールの種類に合わせたメール送信用コンポーネントを作ります (他のコンポーネントとの混在を防ぐために components/mailers などサブディレクトリに作成することをおすすめします)。

先ほど作った Mailer クラスを継承して create メソッドを実装します。 メールの送信に使用する値は、create の引数で受け渡すよりは、 コントローラの viewVars 経由で受け取る方が良いと思います(メールテンプレートのレンダリングでも必要になるので)。

App::import('Lib', 'Mailer');
 
class RegisterRequestMailerComponent extends Mailer
{
  function create() {
    $email = $this->_controller->viewVars['User']['email'];
 
    $q = $this->Qdmailer;
    $q->resetHeaderBody();
    $q->to($email);
    $q->subject('ユーザー登録が完了しました');
    $q->cakeText('', 'register');
  }
}

単体テストではこの create メソッドをテストします(Mailerクラスに対するテストも書く必要がありますが省略)。 特定の入力(引数、viewVars)に対して宛先、送信元、そして本文などが期待通りに作成できるかどうかをテストします。

class RegisterMailerComponentTest extends ComponentTestCase
{
  var $components = array('RegisterMailer');
 
  function testCreate() {
    //viewVarsの登録
    $data = array('User' =>
                  array('id' => 123,
                        'username' => 'xxxxxx',
                        'email' => 'abc@example.com'),
                  'login_url' => 'http://example.com/register');
    $this->Controller->set($data);
 
    $this->RegisterMailer->create();
    $q = $this->RegisterMailer->Qdmailer;
 
    //本文
    $body = mb_convert_encoding($q->content['TEXT']['CONTENT'],
                                'utf-8', $q->charset_content);
    $this->assertTrue(strpos($body, $data['login_url']) !== false);
 
    //宛先など
    $this->assertEqual(1, count($q->to));
    $this->assertEqual($data['User']['email'], $q->to[0]['mail']);
  }
}

このコンポーネントを使用するコントローラからは、send メソッドを呼ぶだけです。

class UsersController extends AppController
{
  var $components = array('RegisterMailer');
 
  function finish() {
    //...
    if($this->User->save()) {
      $this->RegisterMailer->send();
    }
  }
}

より複雑なケース

次に一度に複数のメールを送る場合について考えてみます。 例えば登録完了メールをユーザと管理者に送るとしましょう。 この場合は「ユーザ宛メール(UserRegisterMailer)」と「管理者宛メール(AdminRegisterMailer)」の他に、 それらをまとめる「RegisterMailer」という3つの Mailer クラスを作成します。

UserRegisterMailer と AdminRegisterMailer は単純な例と同じように作ってテストしてください。 RegisterMailer では $components 配列を使ってそれら2つを読み込み、送信処理を委譲します。

App::import('Lib', 'Mailer');
 
class RegisterMailerComponent extends Mailer
{
  var $components = array('UserRegisterMailer',
                          'AdminRegisterMailer');
 
  function sendUserMail() {
    //viewVarsの加工処理など...
    $this->UserRegisterMailer->send();
  }
 
  function sendUserMail() {
    //viewVarsの加工処理など...
    $this->AdminRegisterMailer->send();
  }
 
  function send() {
    $this->sendUserMail();
    $this->sendAdminMail();
  }
}

このクラスに対するテストはモックを使って行うことができます。 本文や宛先の生成処理は移譲先でテストされているので、 移譲先に対する入力の形式と、適切なメソッドの呼び出しが行われていることをテストすれば十分です。

App::import('Component', 'UserRegisterMailer');
App::import('Component', 'AdminRegisterMailer');
Mock::generate('UserRegisterMailerComponent');
Mock::generate('AdminRegisterMailerComponent');
 
class RegisterMailerComponentTest extends ComponentTestCase
{   
    var $components = array('RegisterMailer');
 
  function startTest($m) {
    parent::startTest($m);
 
    $this->RegisterMailer->UserRegisterMailer
	  = new MockUserRegisterMailerComponent();
 
    $this->RegisterMailer->AdminRegisterMailer
	  = new MockAdminRegisterMailerComponent();
  }
 
  function testSendUserMail() {
    $this->Controller->set(array(/* ... */));
 
    $this->RegisterMailer->UserRegisterMailer->expectOnce('send');
    $this->RegisterMailer->sendUserMail();
    //...$this->Controller->viewVars が適切に加工されていることなどを確認...
  }
 
  function testSend() {
    $this->RegisterMailer->UserRegisterMailer->expectOnce('send');
    $this->RegisterMailer->AdminRegisterMailer->expectOnce('send');
    $this->RegisterMailer->send();
  }
}

このコンポーネントの使い方は「単純な場合」と全く同じで、コントローラは一切変更する必要がありません。