Displaying posts written in

11月 2009

jQueryでPrototype.js風のクラスを使う

Prototype.js 風の Class を書きたいが、ライブラリ全体は読み込みたくない、というときのために、必要最低限のコードを抜き出してみた。要jQuery。

var Class = (function() {
        function subclass() {}
        return {
        create: function(parent) {
                function klass() {
                    this.initialize.apply(this, arguments);
                }
 
                var index = 0;
                if(jQuery.isFunction(parent)) {
                    index = 1;
                    subclass.prototype = parent.prototype;
                    klass.prototype = new subclass;
                }
                for(; index < arguments.length; ++index) {
                    jQuery.extend(klass.prototype, arguments[index]);
                }
                return klass;
            }
        }
    })();

最大の違いは $super による親クラスのメソッド呼び出しができないこと。Prototype.jsのチュートリアルの例は次のようになる。

var Person = Class.create({
  initialize: function(name) {
    this.name = name;
  },
  say: function(message) {
    return this.name + ': ' + message;
  }
});
 
var Pirate = Class.create(Person, {
  say: function(message) {
    //親クラスのprototypeに直接アクセス
    return Person.prototype.say.call(this, message) + ', yarr!';
  }
});
 
var john = new Pirate('Long John');
john.say('ahoy matey');
// -> "Long John: ahoy matey, yarr!"

mix-in の例などはそのまま動く。

続・QdmailをCakePHPで使う

以前gistで公開したQdmailerコンポーネントですが、その後もいろいろ手を加えたい部分が出てきたので、正式にgithubにリポジトリを作成しました。

http://github.com/tkyk/cakephp-qdmailer

使い方はREADMEを読んでください。

本家QdmailComponentとの違いは次の通りです。[*]印はgist版からの変更点になります。

  • View クラスの読み込み方法が EmailComponent と同一になっている(そのためThemeView やプラグインが提供するViewも使用可能)
  • テンプレートファイルのエンコーディングが指定されなかった場合、自動判定するのでは無く App.encoding の値を使う
  • CakePHPのConfigure::read(‘debug’)が0のとき、デフォルトでエラーメッセージを表示しない
  • [*]Configure::writeによるグローバルな設定が可能
  • [*]実行時にis_qmailの設定が可能
  • [*]ログの保存先がデフォルトでLOGSになる(オリジナル版ではCOMPONENTS)
  • [*]Qdsmtp使用時に、qdmail.phpをvendorsディレクトリに置くことができる
  • [*]Qdsmtp使用時に、ログの保存先設定が引き継がれる
  • [*]Qdsmtp使用時に、エラーメッセージ表示可否の設定が引き継がれる

is_qmailの設定は非常に重要なので補足しておきます。Qdmailはデフォルトでは「qmailを使っているか否か」をコンストラクタの中で自動判定しようとします。このときシェル経由(system関数)でsendmailコマンドを(qmailでしか使えないオプションを指定して)実行するため、相応の負荷がかかる上に、qmail以外のMTAを使用している場合はmaillogにエラーメッセージが記録されてしまいます。今のところ(qdmail-1.2.6b)この自動判定を実行時に止める方法はありません。そしてCakePHPのコンポーネントとして使う場合はさらに悲惨で、判定がコンストラクタで行われる以上、たとえメールを送らなくても、コンポーネントを読み込むだけで自動判定が実行されてしまいます。

ということで、is_qmailの設定は絶対に明示的に行ってください。具体的にはbootstrap.phpの中でConfigure::writeを使って行います。

Configure::write('Qdmailer',
                 array('is_qmail' => false,
                          /* ... その他の設定 ... */));

なおこのis_qmailの件も含めて作者spokさんには連絡してあるので、いずれ本体側で何らかの変更が行われるかも知れません。

MarkupHelper:他のヘルパーのメソッドを連結する

MarkupHelper を更新。他のヘルパーのメソッドをチェーンして、戻り値をそのままHTMLの中に配置できるようになりました。

http://github.com/tkyk/cakephp-markup-helper

デフォルトでは HtmlHelper と FormHelper のメソッドを、それぞれ h_, f_ というプレフィクスで呼び出せます。こんな感じのコードが記述可能です。

echo $markup
->div('search')
->fieldset->legend->text('ブログ検索')->end
->f_create('BlogSearch')
->f_input('text', array('label' => false)))
->small->h_link('詳細検索はこちら', array('action' => 'search'))->end
->f_end('検索')
->enddiv;

その他のヘルパーを使用する場合はコントローラの $helpers でオプションとして指定します。

var $helpers = array('Markup' =>
                     array('helpers' => array('My', 'Javascript' => 'js')));

標準でヘルパー名がそのままプレフィクスとなりますが、独自のプレフィクスも指定可能です。上の例では MyHelper のメソッドを My_methodName という名前で、JavascriptHelper のメソッドを js_methodName もしくは javascript_methodName という名前で呼び出すことができます。

注意:他のヘルパーからの戻り値は全て html メソッドで連結されます。XSSが起こらないように気をつけてください。

Super Awesome Advanced CakePHP Tips 訳と註5

コアから学ぶCakeのコツ

この章は本当に日本語に訳しにくいです。$options という”英単語”をどう解釈するか、そしてカタカナ語をどこまで許容するかがとても悩ましく、ごまかしも増えてしまいました。内容は別に難しくはないのですが。

Cake Tricks from The Core 註

Cake Tricks from The Core
ここの訳は完全にごまかし。直訳して「コアから、Cakeのコツ」ではあまりにも意味が分からないので作文せざるを得なかった。すいません。原題に「学ぶ」なんて語は無いしそんなニュアンスも無い。ここの from は日本語だと多分「〜発」というのに近い。「コア発のCakeテクニック」みたいなのが良いかも知れない。
Cake Style $options Parameter
このタイトルはそのまま訳すのは不可能。すなわち $options は英語の「option」であると同時に、プログラミング上のテクニック(この節で説明する、$optionsパラメータを使った方法)を指す一種の固有名詞。さらにこの場合の英語の「option」は、自由選択とか自由入力とかいった意味なのだが、日本語プログラミング用語としては片仮名で「オプション」としか言い様がない。オプション引数、つまり関数呼び出し時に値を与えても与えなくても良い、「オプションの」引数を扱うための $options パラメータを使ったCake流のやり方、ということ。
以下いちいち註では書かないが、この節の中の $options という表記の部分には、いずれも同じ問題がある。そのせいで日本語としては非常に煩雑になってしまった。
We’ve all seen functions that …
この章も他の章と同じく、冒頭の文はちょっとだけ格好をつけた感じの文になっている。訳文も雰囲気重視で作っておいた。
a keyed array
「連想配列」と言ってしまいたくなるのだが、連想配列は普通 associative array の訳語だから、ここで使うのは躊躇われる。それにそもそもPHPには「連想配列」と「普通の配列」という区別は無い。同じ一つのデータ構造を使い方によって呼び分けているだけである。次節で分かる通り、著者はそのPHPの特殊事情について自覚的かつ誠実なので、ここでも敢えて keyed という、配列の「使い方」を示す言い回しをしているのだと思われる。「(文字列の)キーを付けられた配列」。
out of whack
「狂った」とか「めちゃくちゃな」といった意味の決まり文句らしい。
This approach
片仮名で「アプローチ」でも大体意味は通じると思うが、「このアプローチではデフォルトのオプションとマージします」では、英語を日本語に訳しているんだか単に片仮名に書き直しているんだか分からない。approach は元々「近付く」という語なので、ここでは「オプション引数を実現する」という目標に対する近付き方、段取りということ。
The end $options
ここでの $options はコード内の変数名(25行目、代入式の左辺)をも指している。
easier to read, shorter to code
語呂の良い文なので、日本語もちょっと工夫した。この code は「コードを書く」という動詞だから、まあ「書く」で通じるだろう。
Handling Data Arrays with a Single Record or an Array of Records
文法的には “a Single Record” or “an Array of Records” 全体に with がかかる形式で、with は漠然と「〜を持って」「〜を備えて」といった意味だと思われる。
since $data is …
いきなり $data という変数名が出てくるが、この節では関数を作ることを想定しているので、これはその関数がとる引数の名前。最後の段落でも出てくる。
telling them apart
tell 〜 apart で「見分ける」。
to do this
形式的にはこの to do this は「to look at the keys」に対応するのだと思うが、実質的にはその次の文の内容「If they are all numeric」も含むと考えて良いだろう。

QdmailをCakePHPで使う

標準で QdmailComponent が付属しているのですが、Cake 1.2 でそのまま使うには少々難があるので、継承して一部メソッドを書き換えたものを gist で公開しておきます。名前は QdmailerComponent です(CakePHP 1.2専用、qdmail.1.2.6b)。

2009-11-15 更新: さらに改良したものをリポジトリで公開しました。続・QdmailをCakePHPで使うを参照してください。

使い方

qdmail.php は verdors ディレクトリに置いて、上のコードを qdmailer.php として components ディレクトリに置いてください。あとは標準の QdmailComponent と同じです。

$this->Qdmailer->to('address@example.com', '日本語名');
$this->Qdmailer->subject('Qdmailerテスト');
$this->Qdmailer->from('from@example.com' , '配信元日本語名' );
$this->Qdmailer->cakeText('テンプレート使用テスト', 'test_template');
$this->Qdmailer->send();

主な変更点は

  • $is_qmail を明示的に設定(本当は Component のオプションで指定できるようにすべきだが)
  • View クラスの読み込み方法を EmailComponent と同一にした(ThemeView なども使用可能になった)
  • テンプレートファイルのエンコーディングが指定されなかった場合、自動判定するのでは無く App.encoding の値を使うようにした

半角カナを全角に変更

ちょっと反則気味ですがレイアウトファイル内で変換するのが一番楽な気がします。

<?php echo mb_convert_kana($content_for_layout, "KV"); ?>

検索条件をページングで引き継ぐプラグイン

このページの内容は古いバージョン(CakePHP1.2)向けのものです。より新しい情報を参照してください。古いバージョンのコードが必要な場合は1.2ブランチから取得してください。


CakePHP 1.2標準の paginate 機能で、複雑な検索条件をページング用リンクに引き継げるようにするためのプラグイン SearchPagination を公開しました。

http://github.com/tkyk/cakephp-search-pagination/tree/1.2

PHP 5.2.xでしかテストしていませんが、それ以前のバージョンでも動くはずのコードなので、もし動かなかったら教えてください。

使い方

検索専用のモデルを作る方法と、作らない方法(他モデルを流用する方法)とがあります。ある程度以上複雑な検索を行う場合は専用モデルを作る方法をおすすめします。ここでは最初に専用モデルを作る方法から説明して、作らない方法については最後の節で説明します。

検索専用のモデルを作る

複雑な検索を行うための肝は専用のモデルを作ることです。検索条件を組み立てるロジックをモデルの中にカプセル化してしまえば、どれだけ複雑な検索フォーム・検索条件にも対応できます。ここでは次のような検索フォームを例に説明していきます。(いろいろ省略されていますが)ブログの記事を検索すると思ってください。

記事検索

タイトル:

投稿日: 日 以降

カテゴリ:

この検索フォームに対応するモデルは概ね次のようになります。buildQuery が検索条件を組み立てるためのメソッドです(記事に対応するモデルは Entry とします)。

<?php
class EntrySearch extends AppModel
{
  var $useTable = false;
  var $_schema = array('title' => array('type' => 'string'),
                       'created_from' => array('type' => 'date'),
                       'category_ids' => array('type' => 'array'),
                       );
  /**
   * buildQuery メソッドの中で検索条件を組み立てます。
   * もし検索フォーム内でソート順や表示件数を指定できるようにする場合は、
   * limit や order もここで設定します。
   */
  function buildQuery()
  {
    $p = $this->data[$this->alias];
 
    $conditions = array();
    if(!empty($p['created_from'])){
      $conditions['Entry.created >= '] = $p['created_from'];
    }
    //....(省略) ...
    return array('conditions' => $conditions,
                 'joins' => $joins,
                 /* ... */);
  }
}

検索フォーム表示部分のビューは次のようになるでしょう。

echo $form->create('EntrySearch', array('action'=>'search'));
echo $form->input('title', array('label' => 'タイトル:');
echo $form->input('created_from', array('label' => '投稿日:',
                                       'after' => '以降'));
echo $form->input("category_ids", array('label' => 'カテゴリ:',
                                        'multiple' => 'checkbox',
                                        'options' => $categories));
echo $form->end('検索');

これでフォームから検索パラメータを受け取る準備ができました。

SearchPaginationコンポーネント

コントローラの中では SearchPagination コンポーネントの setup メソッドを呼び出します。第1引数は検索用モデルの名前です。ここでは EntrySearch を指定します。またオプションの第2引数として、検索フォームの初期値を与えることもできます。下のコードでは検索開始日を1ヶ月前の日付としてみました(この部分のコードは本来はモデルに置くべきかもしれません)。

<?php
class EntriesController extends AppController
{
  var $components = array('SearchPagination.SearchPagination');
 
  function search()
  {
     $oneMonthAgo = date('Y-m-d', strtotime('1 month ago'));
     $this->SearchPagination->setup('EntrySearch', array('created_from' => $oneMonthAgo));
 
     $this->paginate = $this->EntrySearch->buildQuery();
     /*
      ... 必要ならさらに $this->paginate を加工する ...
     */
 
     $this->set('entries', $this->paginate('Entry'));
     $this->set('categories', $this->Entry->Category->find('list'));
  }
}

setup メソッドは自動的に検索用モデルを読み込んで、モデルの set メソッドで値を設定します。だからあとは buildQuery メソッドを呼んで検索条件を組み立て、paginate を実行するだけです。

ビュー

ビューでは何も特別なことはありません。PaginatorHelper を使って普通にページング用リンクを表示してください。sort メソッドも使用可能です。

echo $paginator->numbers(array('modulus' => 12));
 
foreach($entries as $entry) {
  //検索結果の表示
}

生成されたリンクの中には、次のような形で検索条件が含まれているでしょう。

/entries/search/page:2?title=XXXX&created_from=2009-11-01&category_ids[0]=2&category_ids[1]=4

リンクをクリックすれば、これらのパラメータが検索条件として引き継がれます。同時にコントローラの $data プロパティにも引き継がれるため、FormHelper で作った検索フォームにも入力値が自動的に設定されます。

GETメソッドで検索したい

以上でページング用リンクに対する検索パラメータの引き継ぎが行われるようになりました。しかし検索フォームからの最初の検索がPOSTメソッドで行われることが少し気持ち悪いかも知れません。これは date などの複雑な要素を使う場合の、FormHelper の制約です。

この制約を回避するために、SearchPagination プラグインではオマケ機能として、POST用のフォームをGETに変換するJavaScriptを用意しています。動作にはjQueryが必要ですが、他のライブラリに移植することは容易なはずです。次のように使用します。

// jQuery および search_pagination.js を読み込む
$javascript->link('jquery-1.3.2.min', false);
$javascript->link('/search_pagination/js/search_pagination', false);
 
// form に search-pagination クラスを指定する
echo $form->create('EntrySearch', array('action'=>'search', 'class'=>'search-pagination'));

専用モデルを作るほど複雑ではない場合

検索フォームも検索条件も単純で、検索専用に新たなモデルを作るほどではない場合、既存のモデルを検索用に流用することができます。例えば次のようなごくごく単純な検索フォームの場合、

値を受け取るためのモデルとして、Entry モデルをそのまま使ってしまえば良いでしょう。この場合、setup メソッドの第1引数には Entry を渡し、Entryモデルもしくはコントローラの $data プロパティから検索条件を組み立てます。

<?php
class EntriesController extends AppController
{
  var $components = array('SearchPagination.SearchPagination');
 
  function search()
  {
    $this->SearchPagination->setup('Entry');
    if(!empty($this->data['Entry']['title'])) {
      $this->paginate['conditions']
         = array('Entry.title like' => '%'.$this->data['Entry']['title'].'%');
    }
    $this->set('entries', $this->paginate('Entry'));
  }
}

ビューは専用モデルを使った場合と全く同様ですが、これくらい単純なフォームの場合は FormHelper の create メソッドで type => get を指定しても問題なく動きます。

<?php
echo $form->create('Entry', array('action'=>'search', 'type'=>'get'));
echo $form->input('title');
echo $form->end('検索');
 
echo $paginator->numbers(array('modulus' => 12));
 
foreach($entries as $etnry) {
  //...
}