CakePHP-ja tumblr

Dec 26 2009

拡張子に応じてビューを変える

年末進行やべーな。
年末年始の休みを生かして仕事を進めるという前向きの姿勢で取り組んでおります。

Router::connect( '/:controller/:action.:extension' );

と言うような場合に extension の内容に応じてビューファイルを切り替えて欲しいようなケースありますよね。
そういう場合はコンポーネントの beforeRender() とかに拡張子判別の処理を加えておくと簡単です。

class PdfComponent extends Component {
	var $__extension = 'pdf';

	function beforeRender( &$Controller ){
		if( isset( $Controller->params['extension'] ) && $Controller->params['extension'] === $this->__extension ){
			$Controller->layout = $this->__extension;
			$Controller->viewPath = $Controller->viewPath.DS.$this->__extension;
		}
		return parent::beforeRender( &$Controller );
	}
}

上記例は拡張子が .pdf だった場合に PDF 用のビューファイルを読み込むコンポーネントです。
これでレイアウトとビューファイルのディレクトリが拡張子(pdf)のものに変わります。いちいちアクション増やさなくていいので便利ですね。

Nov 21 2009

ページングのURL構造を維持してリンク作成

ページングで一覧するコントローラに絞込み機能などを追加しようと考えた場合、ソートなどページングに関連する URL の状態を崩したく無いのでベースとなる URL をとりあえず維持しておく。

$baseUrl = array_merge( array( 'controller' => $this->params['controller'], 'action' => $this->params['action'] ), $this->params['named'] );

リンク作る場合はこんな感じ

e( $html->link( 'Anchor text', array_merge( $baseUrl, array( 'refine' => 'name:hoge' ) ) ) );

コントローラでこう解釈すればいい

$column = $value = null;
list( $column, $value ) = explode( ':', $this->params['named']['refine'] );
$this->set( 'data', $this->paginate( array( $column => $value ) ) );

+

直接クエリ投げる

いつも忘れるからメモ。
CakePHP の O/R マッパー使わないで直接クエリ投げる方法。


$db =& ConnectionManager::getDataSource( $this->useDbConfig );
if( !$stmt = mysql_query( "QUERY", $db->connection ) ) die( mysql_error() );

PDO化しちゃうのが一番だけどまとめてる時間ない。

Nov 13 2009

Go をインストール

CakePHP 関係ないけど Debian lenny にインストール。

$ sudo aptitude install bison gcc libc6-dev ed make mercurial
$ export GOROOT=$HOME/go
$ export GOARCH=386
$ export GOOS=linux
$ export GOBIN=$GOROOT/bin
$ mkdir $GOROOT/bin
$ hg clone -r release https://go.googlecode.com/hg/ $GOROOT
$ cd $GOROOT/src
$ ./all.bash
$ export PATH=$PATH:$GOROOT/bin
$ mkdir ../my && cd ../my
$ cat >hello.go <<EOF
package main

import "fmt"

func main() {
fmt.Printf("hello, world\n")
}
EOF
$ 8g hello.go
$ 8l hello.8
$ ./8.out

+

cake schema generate と AppModel

コンソールで cake schema generate すると AppModel までが呼び出されて「AppModel のテーブル app_models がありません」などといわれてしまう。 僕の設計の仕方がおかしいのかなんなのかわからないが、ともかく Configure::listObjects(‘model’) が呼び出されて models ディレクトリの・・・・美雪、犯人がわかった!全員をロビーに集めてくれ!!

app_model.php を app/models から app ディレクトリに移動して解決した。

Nov 12 2009

説明力

三流は専門用語と略語をバンバン使った説明をする。

例えば Twitter というサービスの存在を知識として共有している前提で話を進めるため、客はスタート地点からついてこれない。客を煙に巻いて契約を取るのではなく、説明する本人が理解していないから普遍的な言葉で伝えられていないだけ。

二流は専門用語や略語を一切使わずに客が理解するまで懇切丁寧に説明する。

一見すると素晴らしいと思われがちだが、結局のところ種明かしをしているだけで客と自分の知識の隙間が埋まりきってしまうと余裕がなくなる。「このくらいの事ならば3日間で出来ますよ」という先入観をまず与え、その次に「自分ならば3日間で完成させられるのだが、本来1週間は必要な仕事」と、客が理解するまでじっくりと説明するが、大体そんな話は聞いていない。客にとって3日間で終わる仕事なら、それ以上でもそれ以下でもない。つまり、自分からハードルを高くして減点方式での評価を求めている。

一流は専門用語や略語を一切使わずに客が理解したと勘違いする最低限の説明だけに留める。

出来る限り抽象度を高めた説明を行い、客にマクロな視点で評価してもらうよう誘い込む。マクロな視点で見て「要件どおり」と感じられるものを見せて一段視点をミクロな方向に進める。それの繰り返し。
しかし、高い抽象度の説明を理解するには因数分解の如き理解が必要なため、客は本質的には理解できていない。如何にそこを「理解した気にさせるか」がキーになる。

つまり、普遍的な言葉で例えられるのが最強。

+

エンティティ的なDB設計を実装する

CakePHP の O/R マッパーはいささか大仰なものであり、多少なりとも SQL 言語を理解している向きにとっては大味に映る。それはともかく CakePHP でエンティティ的な DB 設計を実装する方法を考えてみた。

まず、entities というテーブルがあり、モデル名は Entity とする。これが全てのテーブルの全てのレコードの実体となるデータを格納するテーブルだ。

CREATE TABLE `entities` (
  `updated_id` int(20) NOT NULL auto_increment,
  `foreign_uuid` char(36) NOT NULL,
  `modified` timestamp NOT NULL default CURRENT_TIMESTAMP,
  `type` varchar(255) NOT NULL,
  `body` longtext NOT NULL,
  PRIMARY KEY  (`updated_id`),
  UNIQUE KEY `foreign_uuid` (`foreign_uuid`),
  KEY `class_name` (`type`)
) ENGINE=InnoDB AUTO_INCREMENT=5114 DEFAULT CHARSET=utf8;

こんなところだろうか。

foreign_uuid は entities テーブルに保存されたレコード、つまりエンティティがどのテーブルのレコードに依存するデータであるかを指し示す。
さらに、type はエンティティのクラス名を指し示すのだが、リソース的な視点から合理的なデータ構造は「インスタンスの中から必要なデータだけを抽出した配列をシリアライズしたもの」であると考えられる。
気になるなら圧縮でもすればよいが、それも同じベクトル上にある対策の一つでしかないので割愛する。

上記のデータを格納するのが body である。
updated_id はデータのシークエンスを保証するだけに過ぎないし、modified は更新日時を記すだけである。

少し横道にそれるが、foreign_uuid となっているのは「全てのテーブルの全てのレコードに対するエンティティを一つのテーブルに保存する」と言う要件を満たすため「全てのレコードで通じる固有性」を UUID で実装した結果である。
CakePHP で UUID を実装するのはとりわけ簡単で、最低限カラム名が uuid となっていれば機械的に解釈してくれる。もしくは String::uuid() がその都度 UUID を返してくれる。

無論、それらの UUID はまだ使用されていない事が保証されたデータではないのだが、entities テーブルに全ての uuid が格納されているのでその辺りの固有性確保はさほど問題ではない。

次に、エンティティの基底クラスとなる EntityModel を実装する。
詳しい実装内容については割愛するが、前述のシリアライズされたデータの再利用性を高めるのであれば最低限 Factory メソッドと Flyweight メソッド程度は揃えておきたい。

DB から抽出したデータを Factory メソッドでエンティティクラスのインスタンスとして再生成し、生データ自体は foreign_uuid でいつでも参照できるようクラスの静的なメンバ変数に格納しておく。
再利用したい場合は Flyweight メソッドで生データから再生成してやればよい。

それらの格納した生データの新規性が損なわれた場合に備えるために init メソッドがあっても良いだろう。init メソッドは新規性云々だけにではなく、例えば「あるカテゴリに属するレコードを抽出する」場合の”あるカテゴリ”を複数回にわたって問い合わせるような場合において必要不可欠なメソッドである。

そうでなければ生データを保持し続ける Factory メソッドに抽出したデータの抽出条件を解釈させなければならず、そんな手間をかけるくらいなら init で全部削除してしまっても良い制御シーケンスを実装すべきだ。

また、連続したデータを扱う以上 Iterator メソッドもあったほうが良い。単純に保持しているデータを Flyweight メソッドで呼び続け、最後までいったら false でも返してやればよい。

ここまで読んだ賢明な仮想読者諸兄ならばお気づきであろうとおり、これは「ハッシュテーブルに複数のインデックスを付与する」実装である。つまり、インデックスの数だけテーブルを増やせば良く、一つのテーブルに対してうんうん唸ってインデックスをひねり出してやる必要はない。
その点この実装は富豪的にリソースを消費するため冗長だと言われればそれまでだが、仕様変更に伴うインデックスの修正も気にする必要はない。新たにテーブルを作ればいいのだ。

ここでひとつだけ留意すべき点がある。複数のテーブルと複数のエンティティがあったとしても、一度抽出したデータを静的に保持するエンティティクラスは大元の基底クラスだけに留めておくべきである。
継承したそれぞれのエンティティはあくまでも入れ物に過ぎない。どのデータがどのエンティティに収まるべきかは基底クラスだけがコントロールできるようにすべきである。そうでない場合リソースのコントロールが極めて煩雑になる可能性が高い。

この点を解決するためだけに基底クラスは基底クラスでなければならないというわけである。

次に、これらのエンティティ的な実装での副産物的効果を考えてみた。

まず考えられるのがモジュラー実装である。
任意のタイミングで任意の処理を走らせる、イベントドリブン的な実装を実現しやすい。

エンティティクラスにイベントのトリガとなるメソッドを用意しておけば良いからである。
例えば、データが抽出された回数を記録しておきたいような場合、Factory メソッドに設置したトリガにフックしたメソッドで記録すれば良い。これだけでインデックスとエンティティに対するロジックをそれぞれから分離することができる。つまり、インデックスの変更についてエンティティを考慮する場面が少なくなるのである。

継承すべきではないメソッドが多数ありクラス間の継承関係をより実際的な使い方をする辺りを合理的な考えだと僕は気に入って使っているのだが、ここまで作ると CakePHP の O/R マッパーが逆に邪魔で仕方なくなってくる。邪魔だからエンティティを採用したのか、エンティティを採用したから邪魔になったのか、その辺りは仮想読者諸兄の想像にお任せするとして、本件はここまでとする。

Nov 09 2009

翻訳言語がわからない場合の対処方法

環境変数 HTTP_ACCEPT_LANGUAGE を取得できない場合、例えば FeedHelper で生成したフィードをフィードリーダーなどで読むようなケースにおいてはフィードリーダーのクライアントプログラムでリクエストするため、HTTP_ACCEPT_LANGUAGE を取得できる可能性は低い。

このようなケースでなるべく適切な言語を選択させるためには環境変数 REMOTE_ADDR を元に国を特定して翻訳言語を見つける方法が考えられる。

例えば、カントリーコードに対するロケールを決めうちで作っておいて http://www.hostip.info/use.html の API を利用してカントリーコードを取得し、作っておいた対応表に照らし合わせてロケールを指定する。と言うような実装はどうだろうか。

無論毎回カントリーコードを見に行くのは無駄なので、キャッシュさせておくかそもそも API ではなく http://www.hostip.info/dl/index.html からデータをゲットして DB に突っ込んでおくなどの実装が現実的。

これだけの仕組みを実装するほど国際的なサービスを作る際にでももう少し掘り下げてみる。

@see http://ja.wikipedia.org/wiki/ISO_639

Nov 07 2009

フォームヘルパーでブラックホール行きまくる場合に気をつけるべき点

セキュリティコンポーネントを利用しつつフォームヘルパーでフォームを生成すると、セキュリティ評価用のトークンが hidden 要素で同時に生成される。
セキュリティ評価用と言ってもセッションに保存されるデータと、フォームの hidden 要素に渡されるデータは全く同じものなので、セッションハイジャック的な攻撃に対しては無防備だと思ったので考えてみた。

セキュリティコンポーネント+フォームヘルパータッグでの働き

  1. セキュリティコンポーネントはリクエストの度にセキュリティ評価用のデータ(トークン)を生成する。
  2. 生成したトークンはセッションに保存。(作成日時など、有効期限の評価に関連するデータも合わせて保存してある)
  3. コントローラーのパラメーターにセキュリティトークンを渡す。

ここまではセキュリティトークンの働き。

  1. フォームヘルパーでは、FormHelper::create() では、コントローラから受け取っているパラメーターにセキュリティトークンが含まれていた場合、その情報を元に hidden 要素としてトークンをフォームに追加。
  2. FormHelper::end() が呼ばれると「今までに生成した入力要素」とセキュリティトークンを元にフィールド用トークンを生成。
  3. hidden 要素などの「リクエストに含まれていても値が固定である」と言う部分を静的、逆を動的とした場合に静的なパラメータはパラメータ名をキーにした連想配列で、動的なパラメータの場合はパラメータをデータとする配列(数値キー)として、それらパラメータで配列を作り、シリアライズして少々の難読化を施してフォームに追加。
  4. 追加が完了したらフォームヘルパーで生成した入力要素の情報を削除。

ここまでがヘルパーの働き。

二つを連携させるのに難しい設定は必要ないけれど、フォームヘルパーで入力要素だけを作ったりしているとどうやってもセキュリティコンポーネントの評価が通らなくなる場合がある。

フォームヘルパーは「今回のフォームから送信されてくるパラメータはこれとこれとこれ」と言う具合に予め制限し、その制限の内容をフィールド用トークンとしてフォームに埋め込んでいる。従って、複数のフォームを設置する場合にはFormHelper::end() で生成したパラメータの情報をクリアしてから新たなフォームを生成する必要がある。

FormHelper::end() ではなく、FormHelper::create() でパラメータ情報をクリアしていればこのような問題には当たらないと思うけれど、逆のパターンだとしても「ロジックの雑さを許容する」分潜在的な危険性を持ったままだと考える事も出来る。

しかし、このやり方ではセッションの所有者である事を保証できない。セッションに保存するのは発行したセキュリティトークンとクライアントの IP アドレスや UA などで生成したハッシュ値をフォームヘルパーに渡すような、客観的な評価基準を設けないと難しいのでは。

Nov 06 2009

Validation::url() が URL の ~ に反応する件への

http://code.cakephp.org/source/cake/libs/validation.php

- $validChars = '([' . preg_quote('!"$&\'()*+,-.@_:;=') . '\/0-9a-z]|(%[0-9a-f]{2}))';
+ $validChars = '([' . preg_quote('~!"$&\'()*+,-.@_:;=') . '\/0-9a-z]|(%[0-9a-f]{2}))';

URL の仕様から見て natinal と定義されてる記号は受け付けないようになってるので、valid と評価されるグループにチルダを追記。

この挙動は仕様っぽい?

Page 1 of 1