Ktai Library 勉強会 #1-2
Ktai Library 勉強会に行ってきました。
といっても、第1回が3月8日(月)で第2回が4月16日(金)でしたので、もうかれこれ1ヶ月近く経とうとしていますが。。。
でも、ちっちゃいことは気にしないで、両日を通して学んだことをまとめつつ、感想などを。(遅ればせながら)
準備編
せっかく作者の@ecworks_masapさんが来るわけですから、できるだけ準備をしておこうというわけで、とりいそぎセットアップを。
CakePHPは1.2を使用しました。
(当日聞いたら1.3でも普通に使えるということで、第2回からは1.3を使用)
Ktai Library は、今後のバージョンアップに備えて、gitで持ってくる。
$ git clone http://github.com/MASA-P/KtaiLibrary.git
(app/vendors/ecw となるように、KtaiLibrary/vendors/ecwをコピーする)
絵文字アイコンが必要なので、それもダウンロード
$ cd app/webroot/img $ wget http://start.typepad.jp/typecast/download/emoticons.zip $ tar xfvz emoticons.zip
Ktai Library で必要なファイルを、app内にコピーします。
以下は、シンボリックリンクで代替。
$ cd app/config $ ln -s /path/to/KtaiLibrary/app/config/ktai_session.php . $ cd ../../app/controllers/components $ ln -s /path/to/KtaiLibrary/app/controllers/components/ktai.php . $ cd ../../../app/views/helpers $ ln -s /path/to/KtaiLibrary/app/views/helpers/ktai.php .
app_controller.php内で、redirect()を上書きするため、KtaiLibrary/app/controllers/ktai_app_controller.phpの内容を、写す。
(ver0.2.2以前の場合で、リバースルーティングに配列を渡している場合は、コレを追加)
すべてのcontroller,viewでktaiコンポーネントとktaiヘルパーを使用する場合はapp_controller内の var $componentsとvar $helpersにKtaiを追加する。
また、以下を環境に合わせて追加
# app/app_controller.php
var $ktai = array(
'use_img_emoji' => true,
'input_encoding' => "UTF-8",
'output_encoding' => "UTF-8",
);
NamedParamsのセパレータを変更
# app/config/routes.php
Router::connectNamed(array(), array('argSeparator' => '~'));
FireMobileSimulator
すでにメインブラウザをGoogle Chromeに完全移行したのですが、いかんせんChromeの拡張機能では、現状ではFireMobileSimulatorのようなものを作れません。
というわけで、ケータイからのアクセスをシミュレートするFireMobileSimulatorをFirefoxにアドオンします。
初めて使ったのですが、超絶便利でした。しかもbetaながら、タブごとに別々のuser_agentに切り替えるバージョンまであり、開発スピードが段違い。素晴らしい!
Hello Ktai Library !
ここまでできたら、Ktai Library の教科書Chapter4のリスト04-04、04-05および04-06を参考に、サンプルページを表示します。
以下、Ktai Library 開発用デモサイトソースより一部改訂して引用
# app/controllers/mypages_controller.php
function index() {}
# app/views/layouts/default.ctp
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<?php if($ktai->is_iphone()){ ?><meta name="viewport" content="width=260"><?php } ?>
<title><?php echo $title_for_layout; ?></title>
</head>
<body>
<?php if(!$ktai->is_ktai()){ ?><div style="width: 240px;"><?php } ?>
<?php echo $content_for_layout; ?>
<div align="center">
<hr width="90%" size="1" color="#333333" noshade>
(C)2009 <a href="http://shop.example.com/">MyShop</a>
</div>
<?php if(!$ktai->is_ktai()){ ?></div><?php } ?>
</body>
</html>
# app/views/mypages/index.ctp
<center><?php $ktai->emoji(0xe753); ?>Hello, ktai Library!<?php $ktai->emoji(0xe753); ?></center>
絵文字付きでHello, Ktai Library!と表示されたら準備完了です。
このブログは記憶を頼りにまとめているので、記述をそのままやっても動かない場合もあると思います。
動かない場合はtwitterでお知らせください。
勉強会 #1
じつは、勉強会の1回目は、第4章を実践することがテーマだったので、準備万端で臨むと、もう終わってたりします(笑)
というわけで私は、今後の開発がしやすいように、いろいろとカスタマイズしていくことにしました。
Themeを使って、KtaiからのアクセスとPCからのアクセスで、表示を分ける
CakePHPのTheme機能を使って、Ktaiからのアクセスの場合は、themed/ktai を使用し、PCからのアクセスの場合は、通常のviewを使用するケースです。
まず、Ktaiからのアクセスかどうかを判別して、必要に応じてテーマを使用するようにします。
Ktaiからのアクセスかどうかは、 Ktaiコンポーネント(かKtaiヘルパー)のis_ktai()を使用します。
# 使用例
if ($this->Ktai->is_ktai()) {
debug("Ktaiからのアクセスです");
}
Themeを分ける場合は、beforeRender()あたりがよさそうですので、app_controller.php内のbeforeRender()を変更します。
# app/app_controller.php
function beforeRender() {
if ($this->Ktai->is_ktai()) {
$this->view = "Theme";
$this->theme = "ktai";
}
}
これでThemeの使用ができますので、必要なファイルを作成します。
まずは、必要なディレクトリを作成しましょう。
app/views内に、themed/ktaiディレクトリを作成し、その中に、layoutsやelements、今回の場合はmypagesを作成します。
$ cd app/views $ mkdir -p themed/ktai/layouts $ mkdir -p themed/ktai/elements $ mkdir -p themed/ktai/mypages
そして、レイアウトファイルを移動します。
$ cd app/views $ mv layouts/default.ctp themed/ktai/layouts
ここで、いったん動作確認。
ケータイによるアクセスとPCからのアクセスで、別々のレイアウトファイルが使われていれば成功です。
mypages/index.ctpも忘れずに移動します。
$ cd app/views $ mv mypages/index.ctp themed/ktai/mypages
その他
elementsでヘッダーやフッターを作ったりして、初日終了。
感想など
@yashioさんの呼びかけで始まったKtai Library 勉強会@関東ですが、非常に素敵な会でした。
まず何といっても雰囲気がよい。少人数故なのかどうかは分かりませんが、堅苦しくなくて良かったです。
また、講義を聞くスタイルではなくて、実践形式で進めるというのが、振り返ってみて良かったなと。講義を聞いて、その場で分かったつもりになって、実践しないでほったらかす人種のわたしとしては、手を動かす絶好の機会となりました。
なかにはセットアップに苦労していた方もいましたが、そういったことも含めて、実際にやってみることの大事さを再確認することができました。
最後は、みんなで歓談し、CakePHPやそれ以外のことについて情報交換。
『ゼッタイに第2回も参加しよう』と心に誓った第1回でした。
勉強会 #2
そして、第2回。今回は第5章ということで、非常にボリュームがあります。
しかし、その多くはPCによる管理画面の作成ですので、ケータイで作る部分はさほど多くない。
ひととおり読み込んで準備して、、、と思ったが、タイミング良く入っているケータイサイトのお仕事を進めないと進捗が遅れていることに気づく。
内容的にはフォームの作成で、サンプルと一緒の画面遷移なので、第5章を参考にしつつ作成しました。
teltoリンク
Ktai Library には、便利なmailto()というメソッドがある。これはいわゆるmailtoリンクを、キャリアに合わせて作成してくれるもの。
# 使用例 (for CakePHP 1.3)
// Site.mail はメールアドレスが入っています
// 第3引数で件名を第4引数で本文を指定可能です
$this->Ktai->mailto("メール",Configure::read('Site.mail'),"携帯サイト問合せ");
telto()もあるかなとリファレンスを見たが、なさそうなので急ごしらえした。
function telto($telno = "") {
if (empty($telno)) {
$telno = Configure::read('Site.tel');
}
$telnoWithoutHyphen = str_replace("-","",$telno);
return "<a href=\"tel:{$telnoWithoutHyphen}\">{$telno}</a>";
}
なんでHtmlヘルパー使ってないんだっけか?相対URLになっちゃうんだっけかな?
おかげで$optionsも指定できないからあんまり意味ないかも。
ハイフンを消す必要があるかどうかも不明だけど、確認するの面倒だからまあ良し。
あと対応できない端末があるかもなあ。
その他
主に見栄えのところを調整した。
Ktai Library には、スタイル調整用の機能があります。
# app/app_controller.php
var $ktai = array(
'style' => array(
'h2' => "color:#ffffff;background-color:#0000ff;",
),
);
としておき、viewで
# view on CakePHP1.3
<div style="<?php $this->Ktai->style("h2"); ?>">見出し文字列</div>
のように使います。便利ですね。
自ヘルパー内で拡張して、divでラップしても面白いかもしれませんね。
感想など
外は雨模様。帰りにはみぞれになっていた。4月なのに。
そんな悪天候の中、参加者は全員出席だったそう。それだけ注目のKtai Libraryであり、Ktai Library 勉強会ということでしょう。
帰ってから実機で確認してみると、キャリアによって細かい動作が違ってくるものがあるということも分かった。
携帯サイトはノウハウの集積が不可欠だと思っていたが、それでもその大部分を担ってくれるKtai Libraryを使うかどうかで、開発効率は大きく違ってくるでしょう。
そして。
Ktai Library を使っても、なお、知っておかなくてはならないことが多いのが、携帯サイトの開発だと気づく。
だからこそ、Ktai Library の教科書では、題材に『ショッピングサイト』を選んだのではないだろうか。
幅広い機能が必要となるため、それを実践することで、必要なノウハウも(多数)身についていくのですから。
続きは次回。
では。
さくらインターネットでbakeする前に
自分用にメモです。
さくらインターネットでCakePHPのbake等を利用する前に、bashへのパスを変更する必要があります。
CakePHPのバージョンアップの時に注意が必要です。
#!/usr/local/bin/bash
以上です。
CakePHPのdatetime validation
かゆいところに手が届くCakePHPですが、バリデーションルールにdatetimeがありません。
(私が知らないだけ?)
ということで、オリジナルメソッドとして、作成しました。
現在テスト中なので、完全ではありません。
# model
var $validate = array(
'any_field_name' => array(
'datetime' => array(
'rule' => array('datetime' ,'ymd'),
'message' => '日付の形式が間違っています',
'allowEmpty' => true,
),
),
function datetime($data ,$format) {
$datetime = current($data);
$s = strrpos($datetime ," ");
$date = substr($datetime ,0 ,$s);
$time = substr($datetime ,$s + 1);
$Validation =& Validation::getInstance();
$result = $Validation->date($date ,$format) && $Validation->time($time);
return $result;
}
(ひとりごと1)突っ込み歓迎(twitterで)
(ひとりごと2)app_model.phpにおくと動かないな。。。
CakePHPのアソシエーション
CakePHPには、非常に強力なアソシエーションという機能があります。
これは相互に関連する複数のModel(テーブル)を一括して扱うための優れた仕組みです。
便利なアソシエーションですが、マニュアルを読んで理解したつもりでも、実際に活用する段になって、どのように設定すればよいのか迷うことがあります。
CakePHPに不慣れな方がアソシエーションで迷っていたときに、どのように教えてあげると分かりやすいのかという視点で、アソシエーションについてまとめてみました。
ここではHABTM(Has And BelongsTo Many)については触れません。それはまた別の機会で。
よくある勘違い
一般にAとBというふたつのモデルがあったとき、AモデルがBモデルを『ひとつ持っているときはhasOne』で『複数持っているときはhasMany』と認識している場合がありますが、その理解のままだと、ときに混乱してしまいます。
よくあるのが、以下のようなケースです。
- ブログの記事を扱ったArticleモデルと、その記事の『編集中・投稿済・削除』等のステータスを扱うStatusモデルがあり
- ArticleモデルとStatusモデルのアソシエーションを検討したとき
- Article hasOne Status と間違ってしまう
ということがあります。
記事が、ひとつのステータスを保持するわけですから、hasOneだろうと考えると、『あれ?どうやってこれ設定するんだ???』となってしまいがちです。
つまり、上記の『ひとつ持っている』『複数持っている』という考え方は、忘れたほうが良いのです。
アソシエーションの基本
それでは、まず、基本的なところからおさらいしつつ、なぜ上述のような混乱に陥るのかを説明していきたいと思います。
アソシエーションの基本原則其の一:外部キーを持っているときはbelongsTo
まず、belongsToからおさらいしましょう。
belongsToは、あるモデル(B)があるモデル(A)に属している時に、B belongsTo A とアソシエーションを設定します。
『属してるってなんぞや?』というと、簡単に言えば、『hasOneもしくはhasManyされている』ということです。
A hasOne B もしくは、A hasMany Bという関係が成り立っている場合、B belongsTo Aというアソシエーションとなります。
hasOne や hasMany が存在せず、belongsTo だけ設定した場合でも、アプリによっては何の支障もなく動作します。
しかし、ここでは基礎を学ぶという意味で、逆方向のアソシエーションの存在と『対』で理解してもらうことから始めます。
外部キー
相手方のモデルの主キー(id)を『外部キー』といい、belongsToが設定されたモデルには、必ずこのフィールドが存在します。
たとえば、上述のArticleモデルが複数のコメントを扱うCommentモデルとhasManyの関係にある場合、
- Article hasMany Comment
- Comment belongsTo Article
ですから、Commentモデルには、article_idというフィールドが必要になります。
逆に言えば、以下の原則が成り立つということです。
アソシエーションの基本原則其の二:hasOneおよびhasManyの相手方のモデルには外部キーが存在する
A hasOne B もしくは A hasMany B というアソシエーションの場合、AモデルにはBモデルの外部キーはありません。
分かりやすいhasManyの例で説明すると、上述の例で、Articleモデルにcomment_idというフィールドを設定することは不可能です。
だってコメントは複数存在できるので、フィールドがひとつでは足りませんから(笑)
hasOne も hasMany と同類
『じゃあ、hasOneならあり得る?』かというと、そういうものではありません。
hasOneの場合、どっちに外部キーを置けばよいか分かりづらいため、外部キーを頼りに考えていくと、より混乱をきたしやすいようです。
そういう場合は、心の中で『hasOne は、hasMany の特殊な一形態』と唱えるようにしましょう。
だって、相手方(関連モデル)が『複数くっついているか、ひとつしかくっつかないか』の差でしか無いわけですから、hasOneなら自モデルに外部キーとなるはずはありません。
もちろんhasOneとhasManyはもっと本質的に違いますが、ここではどのようにアソシエーションすればよいかを考えるために単純化しています
外部キーの位置関係のまとめ
外部キーは、hasOneおよびhasManyのときには『相手方のモデルに存在』し、belongsToしているときは『自モデルに存在』するのです。
この原則を守れば、先頭で提示したマスタテーブルの問題も簡単に答えを導けます。
なお、なぜ上記の原則が正しいのかは、ここでは深く追求しませんのであしからず
マスタテーブルはhasOneではなくhasMany
答えを先に言ってしまいました。
それでは順を追って、なぜそうなるのかを説明していきたいと思います。
例の確認
- ブログの記事を扱ったArticleモデルと、その記事のステータスを扱うStatusモデルがある
- ArticleモデルとStatusモデルのアソシエーションを検討する
- Statusモデルは、editing , published , deleted の3レコードのみからなるマスタテーブル
選択肢はみっつ
hasOneと仮定した場合
- Article hasOne Status
- Status belongsTo Article
hasManyと仮定した場合
- Article hasMany Status
- Status belongsTo Article
belongsToと仮定した場合
- Article belongsTo Status
- Status has??? Article
1: 上記の原則を当てはめて考える
まず、外部キーをどちらに設定するかを考えます。
ArticleかStatusか、どちらになるのかは、あまり考える必要が無いでしょう。
マスタテーブルに外部キーを設定する可能性はありませんから。
つまりStatusには外部キーが存在しないと言うことになります。
ということは。
Article belongsTo Status が確定します。
2: Status has??? Article は、hasOne ? hasMany ?
逆に言えば、Status has??? Article ということになります。
では、hasOneなのかhasManyなのかが問題となりますが、この答えも自明でしょう。
記事は無限に増え続けるわけですから、hasManyでないと、statusが足りません(笑)
Status hasMany Article が確定します。
3: ふたたび上記の原則を当てはめて検証する
結果的にhasManyを用いて、以下のようになることが分かりました。
- Article belongsTo Status
- Status hasMany Article
当初イメージした『ArticleがStatusを持っている』という、やってしまいがちな解釈からいくと、逆になりましたね。
では、外部キーがどうなっているかを考えましょう
-
Article
- id
- status_id
- title タイトル
- body 本文
- Other その他
-
Status
- id
- name
はい。間違いなく、上記の原則に沿っています。
それでも新人くんが迷っていたら
1: hasOneは関係を逆にしてもhasOne
hasOneかhasManyかですが、次のような考え方も成り立ちます。
CakePHPのアソシエーションには『1対多』は存在しても『多対1』はありませんよね。
一方でhasOneは『1対1』ですから、User hasOne Profile が存在する場合。それは、Profile hasOne User でもあるわけです。
つまり『関係性をひっくり返してみて成り立つかどうか』で、A hasOne B か A belongsTo B にするべきかを判断することが可能といえます。
上述のArticleとStatusの例で、Article hasOne Status と(間違って)考えたとしても、逆の関係性=『Status hasOne Article が成り立たないから間違っている』と判断することが可能です。
そんなときはArticle hasMany Status なんて突拍子もないことを考えることなく、Article belongsTo Status ( = Status has??? Article)と考えることができますね。
2: hasMany の場合は、Counter Cache を考える
Article hasMany Comment というアソシエーションがあるとき、Article モデルに comment_count というフィールドを設置し、Commentモデルの(belongsTo)アソシエーション時にcounterCache => trueとすると、commentsテーブルにコメントが追加されたり削除されたりするたびに、自動的にArticle.comment_count にコメント数が入ります。
Articleモデル単体を呼び出すだけで、コメント数が分かるようになり、非常に便利です。
Commentモデルを一緒に引っ張ってきた場合でも、すべてのコメントを取得することなくコメント数が分かります
上述の例に戻ると、たとえば『各ステータス別の記事数が知りたい』という場合、以下のようにするでしょう。
# Status Model
# * id
# * name
# * article_count
$statuses = $this->Status->find('list', array('fields' => array('Status.name', 'Status.article_count'));
debug( $statuses );
// example
// editing => 3
// published => 35
// deleted => 7
Status hasMany Article が正解ということは、Counter Cacheが可能であることからも分かりますね。
まとめ
アソシエーションの基本原則
- 外部キーを持っているときはbelongsTo
- hasOneおよびhasManyの相手方のモデルには外部キーが存在する
迷ったら
- マスタテーブルはhasMany
- 外部キーから考える
- hasOneで良いのか迷ったら、関係性を逆にしてhasOneが成り立たなかったら、belongsTo
- hasManyに違和感があったら、Counter Cache が設定できないかを考え、設定できればhasManyで問題ない
以上です。
アソシエーションは油断すると中級者でも混乱します。
CakePHPに不慣れな新人さんが、アソシエーションで迷っていたときの参考に是非。
参考
関連: モデルを結びつける :: モデル :: CakePHPによる開発 :: マニュアル :: 1.2 Collection :: The Cookbook
【CakePHP】アソシエーションで迷ったらこう考えよう | ECWorks Blog
CakePHPでRSS2.0 feed
CakePHPでRSSフィードをはくのは、ものすごくカンタンです。
今回、当ブログで身をもって体験しましたので、ぜひ。
とはいえ、すでに他のサイトでRSSについては、記事になっています。
(わたしも今回参照しながら対応させていただきました。m(_ _)m)
ぜひ、以下のサイトを参考に設定してみてください。
- RssHelper で RSS フィードを生成する :: RSS :: 主要なヘルパー :: マニュアル :: 1.2 Collection :: The Cookbook
- CakePHP1.2でRSS2.0を出力する[RSS][CakePHP] | Web&MUSICブログ QUALL
- CakePHPのRSSヘルパーの使い方まとめ - 頭ん中
基本的な流れは、上記のサイトの方が詳しいので、そちらを見ていただくとしまして、今回は、Web&MUSICブログ QUALLさんのところで対応していたCDATAについての別解です。
まずは、RSS配信の流れから
ほぼ、上述のサイトそのままですが。
config/routes.phpを編集する
# config/routes.php // 追記 Router::parseExtensions();
controllerでRequestHandlerコンポーネントを読み込む
articles_controller に記事があるとします。
# controllers/articles_controller.php or app_controller.php
// 例
var $components = array('RequestHandler',);
viewにRSS用のviewを作成
レイアウトファイルに標準のものを使用する場合は、viewファイルだけを作ります。
ここで、view内にfunctionを書くのに違和感ある場合は、helperに記入すると良いでしょう。
以下は、そんなケースの例です。
ここではMyHelper (my.php) というのがあるとします。
# views/articles/rss/index.ctp
// 記事一覧が $this->data に入っている場合
echo $rss->items($this->data, array($my, 'rss_transform'));
# views/helpers/my.php
<?php
class MyHelper extends AppHelper {
function rss_transform($item)
{
return array(
'title' => $item['articles']['title'],
'link' => array('action' => 'view', $item['articles']['id']),
'description' => array(
'value' => $item['articles']['body'],
'cdata' => true,
'convertEntities' => false,
),
'pubDate' => ['articles']['posted_date'],
);
}
以上でフィードがはかれます。
デフォルトURLは、 上記の例なら、 /articles/index.rss です。
CDATA対応
上記の description のように配列で記述すると、HTMLタグがそのまま入ります。
HTMLタグをエスケープしたくない場合は、convertEntities を false に、中身をCDATAで囲う場合はcdata を true に、それぞれ設定してください。
というわけで、以上です。
このブログもRSS配信をするために、数時間で対応できました。(現在フィードのテスト中)
上述したブログのおかけで、実際の配信部分は30分程度で完了し、残りはRoutingに苦労したのと、CDATAに対応するために試行錯誤しただけ。
CakePHPさまさまです。
CakePHPのバージョンを取得する方法
CakePHPでプラグインを作っていると、現在稼働中のCakePHPのバージョンが1.2なのか1.3なのか知る必要がでてくるかもしれません。
そんなとき、どうやってCakePHPのバージョンを取得するのだろうと思って、IRCで聞いたら、教えてもらいました。
CakePHPのバージョン取得
debug( Configure::version() ); # ex : 1.2.3.8166
Configureクラスを見てみると、どうやら、coreのconfig/config.phpに値がある様子。
で、ソースを見てみると、、、
<?php return $config['Cake.version'] = '1.2.3.8166'; ?>
コメント除くとこれだけでした。
CakePHPのCache(Viewキャッシュ)について考える
今までさほどシビアにキャッシュについて考えてこなかったのですが、ここらでキャッシュについてまとめてみます。
まずView系のキャッシュのうち代表的なものを。
- element ( & requestAction ) キャッシュ
-
Viewキャッシュ
-
Html Cache Helper
3は、Bakeryに掲載されているMatt氏作成のヘルパーです。初回アクセス時にHTMLファイルを生成するため、以後PHPすら使われません。
多少の制限事項がありますので、それについては、後述。
下に行くほど(1→2→3)キャッシュの威力が大きくなります。
以下順番に見ていきましょう。
1.element ( & requestAction ) キャッシュ
Viewをelement単位でキャッシュしていく方法です。
ヘッダーやフッター、メニューバーやHTMLのheadなど、使い勝手は良いでしょう。
記述の方法は次の通り。
# view
$this->element('element_name' ,array('cache'=>array('time'=>"+2 hours"));
$this->element('element_name' ,array('cache'=>array('time'=>"+2 hours" ,'key'=>$id));
通常は前者で良いのですが、同じelementを別々にキャッシュしたいというときは、後者のようにkeyを渡します。
データが大きいときは'key'=>$id.'_'.md5($data)とでもすれば良いでしょう。
$idは無くてもかまいません。特定のキャッシュファイルを削除するときに分かりやすくしているだけです。
そして、そのelementファイルの中では、requestActionを使用して、データを取得すると良いでしょう。
requestActionは、任意のControllerの任意のactionを個別に呼びだす仕組みです。
右側のカラムにある「○○リスト」等を作成するために重宝します。
# view(element)
echo $this->requestAction('/controller_name/action_name/params' ,array('return'));
第二引数にreturnという値を含めるとレンダリングしたViewを(ただしlayoutは使用せず)返してくれます。
もしデータだけがほしい場合は、actionの最後に return $this->data; のようにしておいて、element側で取得したデータを整形して表示させましょう。
requestActionのパフォーマンスはあまり良くないようですので、elementのキャッシュを使用して効率化してください。
elementのキャッシュファイルを削除する
最後に、requestActionで取得しているデータが更新された場合に、elementのキャッシュファイルを削除する方法です。
ModelのafterSave()とafterDelete()を使用して削除するのが良いようです。
# Model
function afterSave() {
@unlink(CACHE.'views'.DS.'element__action_name');
}
function afterDelete() {
@unlink(CACHE.'views'.DS.'element__action_name');
}
この場合は、当該Modelの全elementのキャッシュが削除されます。
2.Viewキャッシュ
CakePHPのキャッシュは主にこのViewキャッシュで実現します。
このViewキャッシュの最大の特徴は、キャッシュファイルが存在する場合は、Controllerを通らずに、キャッシュされたページを表示する点にあります。
Controller , Component , Model , Behavior , View , Helper 等々のファイルを一切読み込みませんので、非常に高速にページが表示されます。
使用方法は、以下のようにします。
- core.php や bootstrap.php 等で Configure::Cache.check を true にする // Configure::write('Cache.check ', true);
- Controller で CacheHelper を読み込む // var $helpers = array('Cache');
- Controller で cacheAction でキャッシュする時間を指定する // var $cacheAction = 3600; // 秒
$cacheAction には配列を渡して、細かく制御することが可能です。
通常はアクション名(と$id等)を使用します※。詳しくはマニュアルをご参照のこと。
URLルーティングを行なっているときはURLになります。
次のいずれかの設定をした場合にはViewキャッシュは働きません。
キャッシュを(一時的に)使用しない場合には、ここを変更すると一発です。
- Configure::Cache.disable を true にする // Configure::write('Cache.disable', true);
- Configure::debug を 1 以上にする // Configure::write('debug', 2);
3.Html Cache Helper
これはViewをレンダリングした後にHTMLファイルを生成しておき、次回のアクセスからはそのHTMLファイルが直接読みだされるという仕組みのキャッシュです。
生成されるファイルがHTML(正確には非PHP)なので、動的に変化する要素の記述はできませんが、静的ページやRSSなどの書き出しには非常に優れています。
ヘルパーのコードは、HTML Cache - GitHub より取得し、APP/plugins/html_cache に配置してください。(downloadリンクよりzip形式でダウンロードすることも可能)
# directories
plugins/
html_cache/
views/helpers/html_cache.php
使い方は簡単です。Controllerで$helperの最後にヘルパーを記述し、 webroot/.htaccess を編集するだけ。具体的には。
# Controller (他のHelperよりも後に記述)
var $helper = array('XXX','YYY','ZZZ','HtmlCache.HtmlCache');
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_METHOD} ^GET$
RewriteCond %{DOCUMENT_ROOT}/cache/$1/index.html -f
RewriteRule ^(.*)$ /cache/$1/index.html [L]
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php?url=$1 [QSA,L]
</IfModule>
すると webroot/cache以下にURLをもとにファイルが生成されます。(cacheディレクトリはパーミッション707等で作成)
CakePHPは標準の設定で、CSS等ファイルが実在する場合は、ルーティングを行いません。そのため生成されたHTMLファイルが直接WEBサーバーより読み込まれます。
注意事項としては、
・/controller_name 等のURL(action名のないURL)やルートに対してはキャッシュが働かない(仕様変更により可能になりました)
→/controller_name/indexのようなURLにはキャッシュが働きます
・キャッシュファイルの生存期間(expires)は設定できない
→bakeryの記述を参考にcron等で一定期間ごとに自動的に削除するようにします
・パーミッションを適切に設定する
→ファイルの書き出しはwebサーバー等の権限で行われます。webroot/cache以下に書き込み権限がないといけません。また、webroot/cacheはあらかじめディレクトリを作成しておく等の必要があります。
用途と環境に応じて選択しましょう
以上です。
利用する状況に応じてキャッシュの種類を使い分け、快適なサービスを提供していきましょう。
counterCache (HABTMでも)
カウンターキャッシュとは、hasManyしている関連Modelがあるとき、そのModelの件数を元Modelに保持しておく機能です。
http://book.cakephp.org/ja/view/81/belongsTo
たとえばArticle hasMany Comment というコメントを複数ぶらさげている記事があったとき、コメントの件数をArticle Modelに持たせることができます。
これによりわざわざコメントModelを取得することなく、コメントの件数を(記事一覧ページに)表示させるといったことが可能です。
データベースの構造からすると冗長なデータの持ち方ともいえますが、フィールドをひとつ増やすことでDBへのアクセスを減らすことが見込めますので、ぜひ積極的に活用したい機能です。
使い方は簡単です。
HABTMのcounterCacheは後述します。
まず、上記の例でArticleにあたるModelにフィールドを追加します。
- model_name_count integer
上記の例では comment_count です。
つぎに、関連先のModelのアソシエーションを以下のように変更します。
# models/comment.php var $belongsTo = array( 'Article' => array( 'counterCache' => true, 'counterScope' => '', ), );
これだけです。
これだけで、コメントがadd , edit ,delete されるたびに、紐付いたコメントの件数がArticleに書き込まれます。
実際にデバッグモードで発行されるSQL文を確認すると、insert等のあとにselect count(*) して、Articleをupdateしているのがわかります。
HABTMでcounterCacheを使う
さて、非常に簡単なcounterCacheですが、残念ながらHABTMでは使えません。
Article hasAndBelongsToMany Tag ※となっているとき、ArticleにはTagの件数を、TagにはArticleの件数を持っておきたいというのが人情です。
タグ(tag)付けされた、複数の記事(article)がある状況
そこで登場するのが、bakeryに投稿されたこのビヘイビア(CounterCacheHabtmBehavior)です。
このビヘイビアのすばらしいところは、それぞれのModelに xxx_count というフィールドを用意し、$actsAsで指定すれば、それだけで良いということです。
# model
// 両方のモデルに記述します
// カウントはxxx_countというフィールドが存在しているときのみ実行されます
var $actsAs = array('CounterCacheHabtm');
なお、片方のModelのみに xxx_count を用意した場合も、何の設定も必要なく、動作いたします。
(xxx_countが存在しない場合はカウントされません)
CakePHP IRC集会 (2009年07月)
昨日(2009/07/01)は、第3回CakePHP IRC集会が開催された。
今回は「平日の昼間開催」というおもしろい試みでした。
参加人数は約40人。みんな仕事中なのに何やってんだか(笑)
いつもどおりのゆるめの雰囲気で終始し、途中わたしの空気を読めない質問でCakePHP界の大御所たちの貴重な時間を使いつつ、閉会。
閉会後も残ったメンバーの一部が盛り上がり、次回の開催やオフ会の話題で夜まで話は尽きませんでした。
※オフ会は8月上旬に東京で開催予定だそうです。
今回もMASA-Pさんより話がありましたが、返す返すも「CakePHP界隈の雰囲気は良い」のです。
これは貴重なことですね。
さて、最後になりますが、ちょうど開催がCakePHP1.2ガイドブックの発売直後だったということもあり、著者を前にして「買った」「買います」の嵐。
なかには「会社の上司を説得するための必殺トーク教えて」というオーダーも。(しかもそれを著者自ら答えるというサービスぶり)
実際CakePHPガイドブックがなければCakePHPはここまで盛り上がったとは思えません。それくらい1.1版の前著はよくできていました。
その前著を50ページも増ページした今回も間違いなく「買い」です。
CakePHPはRapid Developmentに最適なツールではありますが、慣れるまで(いや慣れてからもしばしば)開発中にはまってしまうことがあります。
生産性が非常に高まるはずのCakePHPも、使用初期はその思想等に慣れるまでどうしても遠回りしてしまうことがあるのです。
そんなとき、マニュアルだけでなく、このような良書を手元に置いておくということは開発初期の生産性を保つための最善の手段です。
「多大の時間の損失を防ぐことができるでしょう」
また、前著をすでに持っている人や、すでに中級者レベルに達している人も、この本を持っておいたほうが良いと思います。
というのは、この本は、CakePHP1.0という初期からCakePHPに関わっているこの世界の重鎮による著作だからです。
ここにはCakePHPを利用するうえで非常に有効な「優れたコード」が満載です。
自分のCakePHPの使い方に「もっと良い方法がないか」と上を目指すためのヒントが見つかることでしょう。
そう考えると、この3,360円という価格は投資額として安価です。
なぜなら必ずやその何倍ものベネフィットをもたらすわけですから。
それではIRC集会に参加したみなさん。また次回。
cakephperさん、どうもお疲れさまでした。
Tree Behavior & Tree Helper
CakePHPのCore BehaviorであるTree Behavior は、簡単にツリー構造のデータを扱うことができる便利なビヘイビアです。
http://book.cakephp.org/ja/complete/91/Tree
さらに、bakeryで公開されている準公式HelperであるTree Helperと併せて利用すると、その簡単さに誰もが驚くことでしょう。
得意なデータはたとえば次のようなものがあります。
- ディレクトリ
- カテゴリ(親子形式のあるもの)
- ユーザーグループ
- その他
基本的に何らかのデータをhasManyするModelが適していると思われます。
例として、複数のArticleを持つCategory Model のテーブルをみてみましょう。
- id integer
- name string
- article_count integer # counterCache用 ※任意
- parent_id integer # Tree Behavior用
- lft integer # Tree Behavior用
- rght integer # Tree Behavior用
- その他必要なフィールド
関連付けは以下の通りです。
# Category Model
var $hasMany = array('Article');
var $actsAs = array('Tree');
# Article Model
var $belongsTo = array('Category' => array(
'counterCache' => true, # counterCache用 ※任意
));
# Categories Controller
var $helpers = array('Tree');
これだけで準備はできました。
なお、Tree Helper は、Bakeryよりコピーしてきてください。
他のappと共用しているpluginsディレクトリがあれば、そこに入れておくとよいでしょう。
基本的な使い方
その1:ArticlesControllerで、addやeditをするときのセレクトボックス用のデータを作成する
# ArticlesController
$categories = $this->Article->Category->generatetreelist(null ,null ,null ,' ');
# views/articles/edit.ctp
echo $form->input('category_id' ,aa('label',"カテゴリ"));
参照:http://book.cakephp.org/ja/view/517/generatetreelist
その2:カテゴリの一覧を取得する
# ArticlesController
$conditions = array(
'Category.article_count >' => 0,
); // Articleが一件以上登録されているもののみ取得する場合
$this->data = $this->Category->find('all' ,compact('conditions') );
これは通常通りに取得可能です。
その3:カテゴリパス用のデータを取得する
# CategoriesController
$parents = $this->Category->getpath( $category_id );
$this->set('parents' ,$parents);
# view
foreach ($parents as $parent) {
$html->addCrumbs(
$parent['Category']['name'] ,
'/categories/index/' . $parent['Category']['id']
);
}
$html->getCrumbs('>' ,'TOP');
viewのコードはHelperにおいておき、layoutファイルから読みだすとスマートです。
参照:http://book.cakephp.org/ja/view/235/getpath
その4:カテゴリの順序を変える
カテゴリの並びを変更するにはふたつのやり方があります。
ひとつは、親カテゴリをつけかえることです。
たとえば以下のようなツリーを想定します。
-
CakePHP
-
View
- element
- ヘルパー
-
Controller
- コンポーネント
- Model
-
View
- レイアウト
- CSS
- HTML
「レイアウト」のparent_idに「View」のidを指定すれば、レイアウトがViewの子供(のカテゴリ)になるので、ヘルパーの次に表示されるようになるでしょう。
# controller
$this->Category->id = 8; // 「レイアウト」の ID
$newParentId = $this->Category->field('id', array('name' => 'View'));
$this->Category->save(array('parent_id' => $newParentId));
では、ヘルパーをelementより先に表示したい場合はどうするのかというのが二つ目の方法です。
「同じ階層の中で順序を変更するとき」はmoveUpおよびmoveDownメソッドを使用します。
マニュアルでは階層を上下させることができるような記述がありますが、あくまで同じ階層のなかで順位を変更するメソッドです。
上記の例でelementをmoveUpしてもすでに同階層の先頭に位置しているため、moveUpは失敗します。
# controller // ひとつ下に順位を下げる $this->Category->moveDown($this->Category->id, 1); # controller // ひとつ上に順位を上げる $this->Category->moveUp($this->Category->id, 1);
参照:http://book.cakephp.org/ja/view/229/Advanced-Usage
マニュアルに詳しい使用例が載っているので参考にしてください。
その5:初期データを設定する
Tree Behaviorを使うときに真っ先に苦労するのが、初期データの登録です。
もともと何らかのデータがある場合、それをインポートするために、単純にphpMyAdminなどでinsertしようとしても、lftやrghtの値を自分でつけるわけにはいかずに右往左往してしまいます。
で、そんなときは、lft・rhtをnullのままにしておき、id,name,parent_idのみを(何らかの方法で)インポートしてください。
あとは、以下のメソッドを実行するだけで、すべてのlft,rghtが割り振られます。
# controller $this->Category->reorder();
admin_reorderというactionにしておくとよいかもしれません。
また、recoverというメソッドもあり、何らかの不用意な操作でTree構造が壊れたときは、これで修復が試みられるそうです。
私は、まだ実際に使ったことはありません。
その6:データの削除
addやeditは通常通りsave()で行いますが、delete()してしまうと、Tree構造が壊れてしまいます。
delete()の代わりにremoveFromTree()が用意されていますので、そちらを利用しましょう。
Tips
Modelであらかじめ $order = array('lft ASC'); としておくと、常に順序よくデータを取得できます。
requestActionと通常のactionを共用することは可能か
requestActionはControllerやView内において、特定のactionを呼びだすときに使います。
たとえば、Viewの一部品であるelementのなかで使用し、「最新のニュースを数件表示する」といった機能を実装できます。
elementにしておけば複数のページから呼びだすことも、キャッシュを効かすことも可能ですので、非常に重宝します。
requestActionの仕様で、URLは「通常のactionとしてアクセス可能」なため、使い方には注意が必要です。
通常のactionとして実行してほしくない場合は、actionの先頭で(requestActionで呼ばれてない場合には)404ページを出力するようにしましょう
CakePHPでは、Viewをレンダリングするのは以下のようなケースです。
- $this->autoRenderがtrue(既定値)で、actionが終了するか$this->render()が呼ばれる、もしくはreturnが返される
- $this->autoRenderがfalseで、$this->render()が呼ばれる
反対に次のケースでは、returnで返された値が直に出力されます。
- $this->autoRenderがfalseで、returnが返される(かactionが終了する)
なお、requestActionによってactionが呼ばれた場合は、あらかじめautoRenderもautoLayoutもfalseとなり、何も指定していなければreturnによって返された値が返ります。
そのため明示的にViewで呼びだしても、layoutされたViewの中に、さらにlayoutされたViewが入るということはありません。
Viewをレンダリングして返してほしい場合は、以下のようにreturnを指定して呼びだしてください。
# view
// 第2引数にreturnを含めるとviewをレンダリングした結果を返す
// layoutは含まないviewの結果のみ
echo $this->requestAction('/controller/action/id' ,array('return') );
このケースでもlayoutはされませんのでご安心を。
requestActionと通常のactionを共用する
さてこれをふまえて、いよいよ本題です。
結論から言うと「共用は可能」です。
同じデータをレンダリングするかどうかの違いであれば、まったく意識することなく共用できますし、以下のどちらかの手順をふめば、requestActionで呼ばれたかどうかは判定可能でしょう。
- actionの先頭でautoLayoutとautoRenderの値をチェックする
- $this->params['requested']を確認する
前者は上述のとおりです。
後者は、requestActionの場合にセットされるようですので、次のように書くことができます。
# controller
function index() {
$posts = $this->Post->find('all');
if (!empty($this->params['requested'])) return $posts;
$categories = $this->Post->Category->find('all');
}
requestActionのときは、余計なデータを読み込むことがなくなります。
追記:Viewキャッシュを使用しているときには注意が必要だった
便利なViewキャッシュですが、どうやら、requestActionで呼ばれた場合でも、すでにページ全体のキャッシュが存在する場合、そのキャッシュを表示するようです。
そのため、elementのなかでrequestActionを呼んだ場合、同じURLのキャッシュが存在するときは、elementのなかにキャッシュされたページが丸々差し込まれるという、非常に不格好なことになります。
さらにさらに、ページ丸々ということは当然にhtmlやheadが入っていますので、そもそもhtmlとしてあり得ない状態になってしまうようです。
Viewキャッシュを使用したページと同じURLをrequestActionで呼びだすことはできないと考えるしかなさそうです。
なぜなら、action内で「requestActionかどうかを判定し・・・」とやろうにも、キャッシュを表示するときはcontrollerを通りません。requestActionがキャッシュを読まない処理を施してくれない以上どうしようもなさそうです。