せっかく私のpatchが採用されたので、ブログにも書いておきます。
CakePHP 1.3からはプラグイン内の DataSource が使えるようになりますが、1.3RC1からは DataSource の driver もプラグインから読み込めるようになりました。
たとえばGithubのCakePHP datasources pluginでは、Cakeコアに含まれない様々なデータベースに対するDboドライバが提供されていますが、RC1ではこれを次のようにして読み込むことができます。
class DATABASE_CONFIG {
var $sqlite3 = array('driver' => 'Datasources.DboSqlite3',
...);
}
また driver を使用する DataSource をプラグインとして提供することも可能です。拙作のKeyValueSourceは次のように使用できます。
class DATABASE_CONFIG {
var $memcache = array('datasource' => 'KeyValueStore.KeyValueSource',
'driver' => 'KeyValueStore.KeyValueMemcache',
...);
}
プラグインが読み込む driver を app/models/datasources 内に作ることもできます。たとえば memcache ドライバを拡張した my_memcache ドライバを作った場合、次のように読み込みます。
// app/models/datasources/key_value/key_value_my_memcache.php
class DATABASE_CONFIG {
var $memcache = array('datasource' => 'KeyValueStore.KeyValueSource',
'driver' => 'my_memcache',
...);
}
拙作のKeyValueSource(key-valueストアのためのDataSource)で、スキーマ・レスなデータを保存できるようにしてみました。
cakephp-key-value-source – GitHub
(このプロジェクトは実用より実験を重視しているので、頻繁に仕様が変わります)
モデルクラスで $looseSchema というプロパティを設定すると、プライマリキーidを除いて、どんな構造のデータでも保存できるようになります。
class SchemalessUser extends AppModel {
var $useDbConfig = 'memcache';
var $looseSchema = true;
}
// in Controllers
$SchemalessUser->save(array('id' => 1234,
'name' => 'John Smith',
'hobby' => array('baseball', 'soccer')));
$SchemalessUser->save(array('id' => 1235,
'firstname' => 'John',
'lasttname' => 'Smith',
'age' => 20));
saveメソッドの fieldList オプションでキーを制限することができます。
$SchemalessUser->save(array('id' => 1234,
'name' => 'John Smith',
'hobby' => array('baseball', 'soccer')),
array('fieldList' => array('id', 'name')));
// hobby は保存されない
// array('id' => 1234, 'name' => 'John Smith')
$ret = $SchemalessUser->read(null, 1234);
$looseSchema プロパティで一部のスキーマを明示的に設定しておくこともできます。下の例ではプライマリキーを key に変更し、更新日時を入力するために updated フィールドを定義しています。
class LooseSchemaUser extends AppModel {
var $useDbConfig = 'memcache';
var $primaryKey = 'key';
var $looseSchema = array('key' => array('type' => 'string', 'length' => 255),
'updated' => array('type' => 'datetime'));
}
// in Controllers
$LooseSchemaUser->save(array('key' => 1234,
'name' => 'John Smith'));
// array('key' => 1234, 'name' => 'John Smith', 'updated' => '2010-02-07 22:23:13')
$ret = $LooseSchemaUser->read(null, 1234);
$looseSchemaを設定せず、$_schema を設定すれば通常の動作になります。
class StrictUser extends AppModel {
var $useDbConfig = 'memcache';
var $_schema = array('id' => array('type' => 'string', 'length' => 255),
'name' => array('type' => 'string'));
}
// in Controllers
$StrictUser->save(array('id' => 1234,
'name' => 'John Smith',
'hobby' => array('baseball', 'soccer')));
// hobby は保存されない
// array('id' => 1234, 'name' => 'John Smith')
$ret = $StrictUser->read(null, 1234);
…
裏側では KeyValueLooseSchemaBehavior というBehaviorを使って、$_schema に存在しないデータを一旦別のフィールドに退避しておき、DataSource の中で取り出しています。退避用のフィールドはデフォルトでは _schemaless_data という名前で、DataSource の describe メソッドの中で組み込まれます。さらに(ちょっと反則気味ですが) Behavior 自体も describe メソッドの中で $actsAs に設定することで、特別な設定なしで動くようになっています。
自動設定される部分を書き下すと、おおむね次のようになります。
class User extends AppModel {
var $actsAs = array('KeyValueLooseSchema' => array('schemalessField' => '_schemaless_data'));
var $_schema = array('id' => array('type' => 'string', 'length' => 255),
'_schemaless_data' => array('type' => 'schemaless'));
}
KeyValueLooseSchemaBehavior の実装は汎用的になっているので、他の DataSource にほとんどそのまま持っていけるはずです。
CakePHP から レコメンデーションエンジン Cicindela にアクセスするためのライブラリを公開しました。
http://github.com/tkyk/cakephp-cicindela
使い方はREADMEを読んでください。Cicindela本体のWeb APIとほとんど同じ構造なので、特に迷うことはないだろうと思います。
ちなみに Configure::read(‘debug’) > 1 で実行すると、DboSourceと同じようにリクエスト情報が一覧表示されます。デバッグに便利です。こんな感じ:
(cicindela) 2 request(s)
| Nr | URL | Status | Took (ms) |
| 1 | http://host/cicindela/recommend?set=pick&op=for_item&item_id=Ruby | HTTP/1.1 200 OK | 2887 |
| 2 | http://host/cicindela/recommend?set=pick&op=for_item&item_id=PHP | HTTP/1.1 200 OK | 1346 |
….
以下、このライブラリを作りながら考えたこと。
このライブラリはDataSource + Behaviorという2クラス構成になっている。最初はDataSourceだけでできるかと思っていたのだが、無理だった。現在のCakeのModelクラスは、DataSourceとDboSourceの関係で言うなら”DboModel”であって、非DboのDataSourceを扱うには適していない。
現在のModelが”DboModel”であるように、XXXSourceに対してはXXXModelが存在することが望ましい。CakePHP 1.3からはDataSourceがプラグイン化できるようなので、同時にModelのベースクラスを提供するようにすれば、ある程度は改善されると思う。ただそれでも結局(プラグインの)ユーザがいちいちクラスの継承関係に気を使わなければいけない点が煩わしい。理想を言うなら、1.非常に簡素なBaseModelクラスを用意する、2.DataSourceにモデル初期化用コールバックを設ける、3.初期化用コールバックにおいてBehaviorをattachする、DataSource固有の機能はBehaviorに実装する……という構成にできれば良いのではないか。
(CakePHP 1.2.5)
ビヘイビアに定義されたメソッドは、モデル自身のメソッドであるかのように呼び出すことができます。
class FooBehavior extends ModelBehavior {
function foo($model){ /* ... */ }
}
class Bar extends AppModel {
var $actsAs = array('Foo');
}
// in Controllers
$this->Bar->foo();
しかしこの方法では Dynamic Finder のように動的に名前が変化するメソッドは実装できません。
ModelBehavior クラスの $mapMethods プロパティを使用すると、この制約を乗り越えることができます。$mapMethods は配列で、次のような構造をしています:
array( '/メソッド名に対する正規表現/' => '実際に呼び出されるメソッド' )
例えば '/^list/' => '_genericList' という設定を行った場合、listから始まる全てのメソッド呼び出しが、ビヘイビアの _genericList メソッドの呼び出しに変換されます。このとき _genericList メソッドの引数には元のメソッド名と引数が渡されます。ちょうど __call によるメソッドのオーバーロードと同じです。
この機能を活用して、あるカラムの値を配列として取得するための listColumn メソッドを作ってみます(Column はカラム名)。このメソッドは ListingBehavior に実装することにしましょう。
class ListingBehavior extends ModelBehavior {
var $mapMethods = array('/^list[a-zA-z0-9]+$/' => '_listColumn');
function _listColumn($model, $methodName, $query=array()) {
$column = substr($methodName, 4);
$query = am($query, array('fields' => array($model->alias.".".$column)));
$arr = $model->find('all', $query);
if(is_array($arr)) {
return Set::extract("/{$model->alias}/{$column}", $arr);
}
return $arr;
}
}
モデルにこのビヘイビアを組み込むと、任意のカラムに対して listXXX メソッドを呼び出すことができるようになります。
class User extends AppModel {
var $actsAs = array('Listing');
}
// in Controllers
$this->User->listId();
$this->User->listUsername(array('conditions' => 'active = 1'));
$this->User->listGroup_id();
メソッド名は小文字に変換されて渡されるので(これが仕様なのかバグなのか微妙なところです)、残念ながら Inflector::underscore を使うことはできません。
key-valueストア用の DataSource サンプルをgithubで公開しておきます。
cakephp-key-value-source – ver.0.1
以下の説明はバージョン0.1を対象としたものです
以下のクラスが含まれています:
- KeyValueSource
- 抽象ベースクラス。サブクラスでは最低限 get/set/del/count 4つのメソッドを実装する
- JsonFileSource
- jsonエンコードしてローカルファイルに保存する実装例
- MemcacheSource
- pecl-memcache を用いる実装例
CakePHPバージョン1.2の DataSource の仕組みはまだまだ未成熟で、あまり突っ込んだ説明は書きにくいので、詳しく知りたい方はコードを読んでください。スキーマ・レスな(スキーマの設定を DataSource側ではなく Model 側で行う)Sourceの例としては最小限の構成になっているはずです。
使用例
database.php
class DATABASE_CONFIG
{
var $json = array('datasource' => 'json_file',
'root' => TMP,
'path' => '/json-encoded');
var $memcache = array('datasource' => 'memcache',
'host' => 'localhost',
'port' => 11211,
'default_expire' => 3600,
'default_flag' => 0);
}
モデル
class Foo extends AppModel
{
//どちらを使ったとしても、基本的な機能を使う限り、全く同じ動作になる
var $useDbConfig = 'json';
//var $useDbConfig = 'memcache';
/*
スキーマの設定
id には length 必須(特に意味がなくても)
*/
var $_schema = array('id' => array('type' => 'integer', 'length' => 4),
'bar_count' => array('type' => 'integer'),
'baz_count' => array('type' => 'integer')
/* ... */);
}
コントローラ
//保存するデータの構造などはDBを使う場合と同じ
$data = array('Foo' => array('id' => 1,
'bar_count' => 1000,
/* ... */));
//保存
$this->Foo->save($data);
//読み出し
$data = $this->Foo->read(null, 1);
//戻り値の構造もDBを使う場合と同じ
$barCount = $data['Foo']['bar_count'];
この程度の使い方だと cache と大して変わらないので、バックエンド固有の機能も少しだけ実装してみました。JsonFileSource は decode オプションを指定することでJSONからのデコード方法を指定できます。false を指定すればJSON(文字列)のまま取り出すこともできます。
//JSONのオブジェクトをPHPのオブジェクトとしてデコード(デフォルトでは連想配列)
$obj = $this->Foo->find('first',
array('conditions' => array('Foo.id' => 1),
'decode' => 'object'));
$bar = $obj['Foo']->bar_count;
//JSON文字列のまま取り出す
$data = $this->Foo->find('first',
array('conditions' => array('Foo.id' => 1),
'decode' => false));
$json = $data['Foo'];
MemcacheSource はモデルの expires プロパティでデータの有効期限を設定できます。
//期限無制限に
$this->Foo->expire = 0;
$this->Foo->save($data);
参考:
- 簡単DataSourceの作り方
- countまわりの実装など参考にしました。こちらではbakeで動かすことが主眼となっていますが、私のサンプルはbakeでは動かないはずです(bake自体使わないので)
※CakePHP バージョン 1.2限定の話題です。
Router::url によるURLの生成時に、prefix パラメータは考慮されない。ありがちな例として携帯用URLを作るために次のような route を追加した場合、
Router::connect('/m/:controller/:action/*',
array('prefix' => 'mobile'));
URL生成時には prefix パラメータの有無によらず、この route がマッチする。各種ヘルパー($html->link | $paginator | $form->create)でPC用のURLに意図しないプレフィクスが付いてしまう、という問題の原因はこれである。
// どちらも /m/foo/bar になってしまう!
$url->link('link', array('controller' => 'foo', 'action' => 'bar', 'prefix' => 'mobile'));
$url->link('link', array('controller' => 'foo', 'action' => 'bar'));
この問題を回避するためのイディオムとして、prefix パラメータの他に [prefix名] => true というパラメータを設定する。
Router::connect('/m/:controller/:action/*',
array('prefix' => 'mobile',
'mobile' => true));
URL生成時にはこちらのパラメータを使う。
// こちらは /m/foo/bar
$url->link('link', array('controller' => 'foo', 'action' => 'bar', 'mobile' => true));
// こちらは /foo/bar
$url->link('link', array('controller' => 'foo', 'action' => 'bar'));
参考:
- 3.4.5.5 プリフィックスルーティング(Prefix Routing) :: The Cookbook
- 現在は正しいコード例が示されている。
- “Prefix利用時のPaginatorHelperが吐くURLが正しく表示されない” フォーラム – CakePHP Users in Japan:
- cakephp.jpのフォーラムにて。検索パラメータが引き継げないのはおそらく connectNamed が設定されていないから?
なお Routing.admin によるルーティングは純粋なプレフィクスルーティングではなく、Router の中で特別扱いされているのでこの問題は生じない。また CakePHP 1.3 ではプレフィクスルーティングの機能は大きく拡張されるため、最終的にどのようになるのかは分からない(コードを斜め読みした限りでは、個々の route に個別に設定する必要は無くなり、Configure::write(‘Routing.prefixes’, array(…)); で一括指定できるようになるらしい。ただ少なくとも 1.3.0-alpha においては、1.2同様の prefix も使えるようだ)。
追記:
[prefix名] => true を設定してもうまくプレフィクスが付かない場合は、route 側にパラメータが足りない可能性が高い。最もありがちなのは名前付きパラメータを使用する場合。
// こんなURLを作りたいが => /m/foo/bar/type:99
// こうなってしまう => /foo/bar/mobile:1/type:99
$html->link('link', array('controller' => 'foo', 'action' => 'bar', 'mobile' => true, 'type' => 99));
こういうときは Router::connectNamed を使って routes.php の中で宣言しておく必要がある。
Router::connectNamed(array('type'));
Router::connectNamed についての説明はAPIドキュメントが最も詳しい。ちなみに page や sort といった paginator 関連のパラメータはデフォルトで設定されるので改めて設定する必要はない(だからこそ気が付きにくいとも言えるが)。
以前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 を更新。他のヘルパーのメソッドをチェーンして、戻り値をそのまま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が起こらないように気をつけてください。
この章は本当に日本語に訳しにくいです。$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」も含むと考えて良いだろう。
標準で 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"); ?>