CakePHP 1.3.11 と 2.0.0-beta のリリース(訳)
CakePHPコアチームはCakePHP 1.3.11 と 2.0.0-betaをこんなにも早くお見せできることを誇りに思います。1.3.11は1.3ブランチのバグ修正・メンテナンスのリリースです。2.0.0-betaはAPIの安定化の開始と、また後方互換性のない変更をこれ以上するべきでないポイントを位置づけるものですが、まだ商用システム向けということではありません。5月の下旬にCakePHP 1.3.10がリリースされてから、27以上のコミット*1と10個のチケットが解決されました。アプリケーションに影響する可能性のある変更がいくつかあります:
1.3
- Fixtureは逆順にクリア(truncated)されるようになりました。これはデータベースの制約(constraints)を伴ったアプリケーションがよりスムーズにテストを走らせることを可能にします。
- デバッグ出力からデータベースの認証情報を隠し、本番サーバーでdebug = 0とするのを忘れたユーザーを保護します。
- mod_rewriteが正しくセットアップされているかの検出のバグが修正されました。
- データソースはMysqlまたはMysqliに接続できないとき、エラーを引き起こすのではなく、falseを返すようになりました。
- 重要なCakePHPのアナウンスを表示するために、デフォルトのhome.ctpにバナーを追加しました。
2.0
2.0でコアチームは、次世代フレームワークのためのTODOリストの残り項目を実装することにおいて、大躍進を成し遂げました。私たちは新しいコミッターの大きな潮流と多くのgithubでのプルリクエストを得ることができました。コミュニティがCakePHP 2.0に興味を置いていることにワクワクしています。2.0.0-alphaから、多種多様なトピックブランチのマージを伴って、375以上のコミット*2があります。もし既にアプリケーションが2.0で動いているなら、それをアップデートする必要がある2つの大きな変更があります。最大の変更はディスパッチ(Dispatching)処理が、webrootのindex.phpファイルでCakeResponseのインスタンスを渡すことを必要とするようになるということです。また、デフォルトのルートが、アプリケーションのroutes.phpからインクルードされる必要のある専用のファイルから展開されるようになりました。このことから、app/Config/routes.phpをデフォルトで設置されているものに従って書き換えるようにしてください。このリリースの重要なポイントのクイックリストは以下の通りです:
- Microsoft SqlServer 2008 と 2011 は第一級市民となりました(訳注:手堅くサポートされたということ)。
- フォームヘルパーはモデルのメタデータを再度推測できるようになりました。コントローラが使っているモデルが何かを表すコントローラの$usesプロパティを用いて賢くヘルパーにヒントを与えることができるようになりました。これは1.3アプリケーションで$usesがパフォーマンス低下の要因になり得ると思われたことに焦点を当ててます。これはもはや2.0ではなり得ません。
- i18n展開シェルは処理にプラグインを除く、またはプラグインごとに展開することができるようになりました。
- モデルのバリデーションメッセージをi18nシェルによって自動的に展開することができるようになり、もはや1.3アプリーケーションのようにコンストラクタでメッセージを翻訳する必要がなくなりました。
- デフォルトのルートは専用のファイルに設置され、app/Config/routes.phpでインクルードされる必要があります。必ず更新してください!
- DispatcherはCakeResponseを二番目の引数で受け取るようになりました。必ずapp/webroot/index.phpファイルを更新してください!
- Model::saveAll()はリファクタリングされ、モデルがsaveMany、saveAssociated、validateMany、validateAssociatedといったメソッドをもつようになりました。
- Model::find()はリファクタリングされ、クエリー生成処理が新しいbuildQuery()メソッドでされるようになりました。
- 数多くのパフォーマンス向上。
(訳注:翻訳元で多分ここに改行が足りてない)
この時点から、CakePHPコアチームはlighthouse*3に現れるバグの修正と、gitベースでsphinxを使ってコンパイルされる新しい2.0のマニュアル*4(book)に注力します。素晴らしいドキュメントを手に入れることで私たちに手を貸そうと思っていただけるなら、どうぞ自由にレポジトリをフォークして変更のプルリクエストを送ってください。また、APIドキュメント化の向上を望んでもいますので、コードでのドキュメントブロック(doc blocks)のプルリクエストもまた非常に歓迎します!
CakeFest 2011*5が近くなったことで、開発者に早期割引価格が過ぎ去ってしまう前のチケットの購入を推奨しています。ワークショップトレーニングの証明書の授与、2トラック並行のトークセッション、多くの国際スピーカー、などなどを含んだとても多くのサプライズをこの9月のマンチェスターでお見せすることができることでしょう。まだ悩んでいるのなら、スケジュール*6を確認しておいてください!
全ての関連するコミット、チケット、ドキュメントの編集、その他フレームワークへの助力を通しての貢献に関して、多大な感謝を捧げます。あなたがいなければ、CakePHPはなかったことでしょう。
-- 訳ここまで
後書き
Bakeryのリリース記事の訳です。
http://bakery.cakephp.org/articles/lorenzo/2011/07/26/cakephp_1_3_11_and_2_0_0-beta_released
2.0のベータリリースはまだまだ先の9月ぐらいかなーと個人的に思っていましたが、もうリリースされましたね。はええ。
これは年内stableリリースも見えてきましたね。ドキドキ。
何も考えずにSlugRouteを生成する
SlugRouteとは
単純に、
/posts/view/127
といったURLでは見栄えがあまり良いとは言えません。
そこで、例えばブログのタイトル等を使って、
/post/何も考えずにSlugRouteを生成する
といったURLにすることによって、SEO有効性も、URLを見た時の関心の引き具合も、ぐっと引き上げることができます。
これらのURLを扱うルーティングをまとめてSlugRouteと呼ぶことにします。
※そもそものSlugの定義とは少し違いますが、他の呼び方が思いつきませんでした。誰か教えて!
忍者プラグイン
まだドキュメントが出来上がっていませんが、忍者プラグインというものを作りました。
plugins/ninjaに適当に設置してください。(プラグインの設置はプラグイン :: CakePHPによる開発 :: マニュアル :: 1.3コレクションを参考にしてください。)
この中に以下のカスタムルートクラスが二つあります。
- AutoSlugRoute
- MultiSlugRoute
これらのどちらかを使うことにより、今すぐSlugRouteを作ることができます。
共通の機能としては、ざっくり
- 規約を使ったモデル、フィールドの自動決定
- モデル探索のキャッシュ
- キャッシュの削除
があります。
AutoSlugRoute
例えばdisplayField(titleなど)が設定されていれば、以下のようにするだけです。
これだけで、:titleにPost.titleが埋めこまれます。
また、様々な状況に対応するオプションは以下のものがあります。
MultiSlugRoute
複数のスラッグを使いたいときに用います。
基本的にAutoSlugRouteと同じですが、指定方法がいくらか変わります。
基本のモデルに紐づいたモデルも利用可能です。
オプション:
その他コールバックが実装できるなど、MultiSlugRouteはAutoSlugRouteより高機能です。
注意点
AutoSlugRouteはデフォルトで全件をキャッシュします。
これは高速に動作しますが、キャッシュ生成時に負荷が大きいため、局所的にパフォーマンスの劣化が起きる可能性があります。
MultiSlugRouteは必要な時にクエリを発行し、キャッシュをアップデートします。
これは全体的に満遍なく負荷がかかることを意味します。また、キャッシュのアップデートが頻繁にかかるため、全体的なパフォーマンスに気をつける必要があります。
以上のような問題を解決するために、細かいキャッシュコントロールを行う必要があるかもしれません。
また、1万件以上レコードのキャッシュが発生すると、キャッシュ読み込みの負荷が著しくかかります。(検証済みです)この場合、キャッシュをオフにすることも検討する必要があるでしょう。
これらパフォーマンス問題は、トレードオフな側面があります。
開発する規模によっては、コンポーネント単位での制御、またはIDによる制御をしないという設計もまた有効です。
詳細なキャッシュコントロールメソッドについては、ドキュメント、または解説をブログの記事にします。
最後に
レコードが固定、CMS、ブログなどという要件であれば、まったく支障なくSlugRouteが一瞬で作れ、堅牢です。
是非一度お試しください。
フィードバックお待ちしております。
CakePHPアプリケーションの基本的な設計指針 (2) - キャッシュまわり -
イントロダクション
CakePHPのキャッシュ機構は、
- キャッシュストレージへのラッパー
- コアに統合されているもの
の2種類があります。
前者はCache::read()などを使うもので、自由にキャッシュの操作が行えます。
後者のほうは、ビューキャッシュ、クエリキャッシュ、ディレクトリマップのキャッシュなどで、仕組みを理解した上で設計を思慮する必要があります。
特に問題となるのはビューキャッシュです。
ビューキャッシュの生成・破棄
ビューキャッシュは、エレメントキャッシュと、アクションキャッシュ(フルページキャッシュ)があります。
アクションキャッシュでは、生成されるファイル名の規則としてURIを用いています。
これにより、ルーターの起動の前にキャッシュ処理に移行することを可能にしています。
例:(以下、パスはAPP/tmp/cache/views/以下を示します。)
さて、usersテーブルに変更があったとき、/users/view/1などのキャッシュが残っていると、古い内容を表示したままになってしまいます。
そこでモデルからデータ操作をした際、これをCakePHPは自動で破棄します。
消されるファイルのフォーマットは以下のようなものです。
*users.php *users_*.php
同時に関連モデルについても消されます。
また、PostCommentのように複数の単語からなる名前は、また少々異なります。
*post_commnets.php *postcommnets.php *post_commnets_*.php *postcommnets_*.php
これらの元となるのは、モデルのエイリアス名の複数形であることに注意しましょう。
例えば、
<?php Router::connect('/ranking', array('controller' => 'users', 'action' => 'ranking')) ?>
といったルートがあり、ビューキャッシュされている場合、これは破棄されないことになってしまいます。
同様に、/user/:nickname/なども該当します。
このようなビューキャッシュの破棄の挙動を変えるには、Model#_clearCache()をオーバーライドします。
よって、ルートをデフォルト以外のものに変える際は、そのビューキャッシュが消えるように設計することが望ましいでしょう。
例えば、次のような設計が考えられます。
- モデルのエイリアスの単数形を破棄対象に加える
- アプリケーション特有のURL規則に則ったものを破棄対象にする
- routes.phpでルーティングするURLをどこかに保持しておく
- カスタムルートクラスを使ってルートの情報を保持しておく
- モデルでなく(空のメソッドでオーバーライド)、コントローラ(コンポーネント)でキャッシュを破棄する
- データに変更があった場合、ビューキャッシュは全て消す。データの変更がメンテナンス時にしかありえないときなど、場合によってはあり得る選択肢です。
- 全て手動で消す(頑張ってください :P)
ユーザエージェントによるビューの振り分け
ビューキャッシュにおいて、同じURLのキャッシュは同等のものとみなされます。
よって、PCと携帯で同じURLを見ているとすると、PCで携帯のキャッシュが、逆に携帯でPCのキャッシュが現れてしまうことがあり得ます。
このことから、少なくともビューキャッシュを使う限り、コントローラやアクションはユーザエージェントごとに別にしてしまったほうが良いでしょう。
コントローラが肥大化するという危惧をするかもしれませんが、コントローラの継承、モデルへのfindオプションの保持を行っていれば、それもあまり問題にはなりません。
ただし、設計時にコントローラの継承などはコンポーネントのマージなど十分気をつけなくてはなりません。これについては機会があったら示します。
SSLを交えたビューキャッシュ
SSLページと非SSLページで同等のURIの場合、勿論同じビューキャッシュが適用されます。
あるページでSSLでも非SSLでも表示させる必要がある場合、外部サービスのアセットがブラウザにより遮断されてしまう恐れがあります。(twitterのつぶやきボタンなど)
その場合は、//example.com/util.jsなどといった、HTTPとHTTPSの判断をブラウザに任せる記述を用いるのが適当です。
しかし、これはヘルパやルータなどでうまく反映されず、/path/to/webroot//example.com/util.js などといったパスに変換されてしまうことがあります。(2.0では対応されています)
その場合は、AppHelper#url()をオーバーライドするなどして対処しましょう。
<?php class AppHelper extends Helper { public function url($url, $full = false) { if (is_string($url) && preg_match(sprintf('/^%s.+/', preg_quote('//', '/')), $url)) { return h($url); } return parent::url($url, $full); } }
エレメントキャッシュ
固定的なウィジェットを表示するために、AppController#beforeRender()/beforeFilter()などで常にDBの読み込みが発生することは、致命的なパフォーマンス問題を引き起こします。
そういったものはrequestAction()を用いたエレメントをキャッシュすることが望ましいでしょう。
詳しくは
requestAction :: その他の便利なメソッド :: コントローラのメソッド :: コントローラ :: CakePHPによる開発 :: マニュアル :: 1.3コレクション
を見てください。
フレームワークから離れる
一般的に言えることですが、外部サービスのストレージを利用する場合など、遅いストレージのものは常にキャッシュすべきです。
しかし、単に画像をキャッシュする場合など、CakePHPのアクションを通してしまうと、余計な負荷がかかってしまう場合があります。
キャッシュ機構自体をhttpdのモジュールなどで対処することもスマートな解決策の一つですが、ここは純PHPに任せてみましょう。
- APP/webroot/tts.php
<?php // 一例。ストリーミングなどを使ったほうが当然良いが、コードが複雑化するだけなので省略する // 「チャンク変換考慮してないの?」 => 複雑化省略 if (empty($_GET['word'])) { header('HTTP/1.1 403 Forbidden'); exit; } $word = $_GET['word']; $word = rawurlencode($word); $cacheKey = 'tts_word_' . $word; $cached = apc_fetch($cacheKey, $success); if ($success) { foreach ($cached['headers'] as $header) { header($header); } echo $cached['contents']; } else { $contents = file_get_contents("http://translate.example.com/?word=$word"); if (!$contents || empty($http_response_header) || !is_array($http_response_header)) { $forbidden = 'HTTP/1.1 403 Forbidden'; header($forbidden); $contents = null; $headers = array($forbidden); } else { $headers = array(); foreach ($http_response_header as $header) { header($header); $headers[] = $header; } echo $contents; ob_end_flush(); } // caches a week apc_store($cacheKey, compact('contents', 'headers'), 60 * 60 * 24 * 7); }
- 呼び出し
<?php echo $this->Html->link($word, '/tts.php?word=' . rawurlencode($word)) ?>
これはもちろんサービスがプライベートアクセスを要求する場合は使いにくい手法です。
CakePHPのキャッシュは素晴らしいですが、それを使わないという手段があることも頭の片隅に入れておくと良いかもしれません。
最後に
ビューキャッシュの仕組みは複雑で扱いにくく、辟易するかもしれませんが、使い方によっては強力な武器になります。
ハマりどころが分かっていればそれなりの対処はできるはずです。
CakePHP2.0では改善した部分もあるので、是非窓からPCを投げ捨てないように付き合ってあげてください ;)
補足:Togetter - 「CakePHPアプリケーションの基本的な設計指針 (2) - キャッシュまわり - へのツッコミなど」
CakePHPアプリケーションの基本的な設計指針 (1) - URL設計 -
イントロダクション
CakePHPはMVCデザインパターンを採用しており、基本的にこれに従った設計が基本となります。
しかし、ある程度開発が進んだところで、やっかいな設計の問題にぶち当たることは多いですよね。
そこで、よくある問題を取り上げて、設計の指針としてまとめてみることにします。
注:とても1回で書ききれるとは思えません。連載となる可能性が高いです。
URL設計
CakePHPのURLは、最初は独特に見えたかもしれません。
http://example.com/users/view/1 などよりも、http://example.com/user/hiromi2424 などの見栄えを期待していた方も多いのではないでしょうか。
しかし、だからといって以下のようなアクション、URLの呼び出し方は、後々に響いてきます。
<?php // Route Router::connect( '/user/:nickname', array( 'controller' => 'users', 'action' => 'view' ), array( 'pass' => array('nickname'), ) ); ?> <? // Controller class UsersController extends AppController{ function view($nickname) { $this->set('user', $this->User->findByNickname($nickname)); } } ?> <?php // View echo $this->Html->link($user['User']['nickname'], '/user/' . $user['User']['nickname']); ?>
大きく3点の問題があります。
- URLを呼び出す時に、User.nicknameが必要(user_idしかなかったらどうする?)
- ニックネームの重複が許されない
- ルートが変更された場合、ビュー、コントローラなどの該当URL呼び出しを全て変更する必要がある
IDをURLに含めるかは要件次第ですが、たったこれだけのことで後に重大な変更になりかねません。
URLにIDが含まれていなくても、IDを元にアクションの引数、URLを扱うように設計していればその心配がなくなります。
また、CakePHP1.3からは、カスタムルートクラスが利用できます。
これを利用することにより、標準的なアクションの形で様々なURLを利用することが可能となります。
<?php // Route App::import('Lib', 'route' . DS . 'UserNicknameRoute'); Router::connect( '/user/:id/:nickname', array( 'controller' => 'users', 'action' => 'view' ), array( 'routeClass' => 'UserNicknameRoute', 'pass' => array('id'), ) ); ?> <?php //RouteClass class UserNicknameRoute extends CakeRoute { function match($params) { // controller, actionの比較 if (array_intersect_key($params, $this->defaults) != $this->defaults) { return false; } if (isset($params[0])) { // 実際はキャッシュするべき $params['id'] = $params[0]; $params['nickname'] = rawurlencode(ClassRegistry::init('User')->field('nickname', array('id' => $params[0]))); unset($params[0]); } return parent::match($params); } } ?>
URLがニックネームが違っていてもマッチしてしまう場合は、カスタムルートクラスのparse()を実装するか、コントローラ(コンポーネント)でチェックなどをして工夫しましょう。
また、文字列でのURLの呼び出し('/user/' . $user['User']['nickname'])は極力しないようにしましょう。
ただし、例外はあります。
- ホームページ('/'を指定)
- 画像などアセットのパス
- 外部URL
など。
呼び出すURLがアクションの場合、必ず標準的な配列形式で指定するようにしましょう。
最後に
これらCakePHPの機能に配慮した設計を行うことにより、変更に強く、また柔軟なルーティングが行えます。
億劫な配列地獄に見えるかもしれませんが、そこにはちゃんと規約のマジックが存在するのです。
#URL設計だけで1回終わってしまった・・・どうなることやら。
誰でもできるサービスレイヤの実装
イントロダクション
CakePHPでは標準でサービスレイヤを提供していません。
しかし、CakePHPはフレームワークとしてその基礎部分は十分に提供してると言えます。これはアプリケーション側で実装すべきでしょう。
規模が一定を超えるアプリケーションではサービスレイヤの効能は特筆すべきものがあります。
それは「何をしているか」を端的に表すことができ、開発初期段階において「何が必要か」を明確にし、更にテストを容易にします。
また、トランザクションのサポートもサービス単位で実装することによって、明確に「1サービス1トランザクション」が表現できます。
サービスをモデルとして実装する
上記のサービスモデルは非常に単純化されたサービスモデルの一例です。
これは各モデルのマジックメソッドによる呼び出しを提供します。
saveNewArticle
はその例で、使うモデルを一々定義する必要はありません。
コントローラなどからサービスを実行する
サービスレイヤを利用する場合、コントローラ特有のモデルの読み込みはもはや必要ありません。
ただし、IDの自動設定や各種コンポーネントのコントローラのモデルクラスの検出などを利用したいときは、手動でClassRegisrty::init()を用いてサービスクラスを読み込むべきでしょう。
コンポーネント化しても良いですね。
このようにして利用します。
また、シェルでも同じものを利用したい場合があります。例えば、メールからの記事の投稿です。
これを
こうします。簡単ですね。
サービスのテスト
以上までで、サービスレイヤの単純な構築は完了です。ここからは、より高度なサービスの利用を考えます。
まずはテストです。
サービスのテストにはモックが便利です。
これは開発初期段階で、サービスモデルで呼び出すモデルのメソッドやプロパティなどを定義し、後に該当のモデル(ここではArticle)を実装することにより、設計 => 実装をコード単位で表現することが可能になります。
また、モックを利用するには空のArticleモデルを作成する必要があります。
トランザクション処理
ここまできたらトランザクション処理も実装しましょう。
上記の例を拡張したものを考えます。
トランザクションを実装するモデルと、それを中継した呼び出しをすれば良いです。
これでサービスにトランザクションが実装できました。テストは以下のようになるでしょう。
トランザクションの結果までテストすることができましたね!
最後に予想はできるでしょうが、コントローラでの呼び出しはこうなります。
これで非常に叙述的な書き方ができるようになりました。
最後に
アプリケーションが要求するサービスの仕様はもう少し複雑になるかもしれません。
しかし、サービスレイヤのもたらす恩恵は意外に大きいです。
この考えは何もCakePHPに限ったものではないので、習得することをお勧めいたします。
KcaptchaプラグインでさくっとCaptcha実装
Kcaptchaプラグインとは
このプラグインは、KCAPTCHAをものすごい簡単に使おうという趣旨のもと作られました。
単純な使い方ではほんとにあっという間に使うことが可能になります。
hiromi2424/CakePHP-Kcaptcha-Plugin - GitHub
上記でこのプラグインをMITライセンスで公開しています。
以下にREADMEの抜粋を訳したものを載せておきます。
インストール
pluginsディレクトリで、
git clone git://github.com/hiromi2424/CakePHP-Kcaptcha-Plugin.git kcaptcha
git submodule add git://github.com/hiromi2424/CakePHP-Kcaptcha-Plugin.git plugins/kcaptcha
とします。または、ダウンロードして手動で設置してください。
アプリケーションのvendorsディレクトリにkcaptchaを設置することによりそっちを参照することも可能です。
kcaptchaのカスタマイズした設定があるなら、この方法をとってください。
使い方
コントローラにCaptchaコンポーネントを設置してください:
<?php class PostsController extends AppController { public $components = array( 'Kcaptcha.Captcha', ); }
フォームでコンポーネントによって自動的にセットされるCaptchaヘルパーを使ってください:
<?php echo $this->Captcha->render(); echo $this->Form->input('captcha'); ?>
単純にこれで終わりです!これでPostモデルはcaptchaのためのユーザの入力のバリデーションを行うことでしょう。
更なるオプションやカスタマイズ機能については、以下のAPIリファレンスを見てください。
APIリファレンス
Captchaコンポーネント
設定:
$sessionKey
-string
captchaに用いるセッションのキー。デフォルトは'Kcaptcha.answer'
です。これを変更すると、手動でrender()
を使うアクションをコントローラに作らなければなりません。$model
-string
バリデーションで使うモデル名。デフォルトはコントローラの$modelClass
です。$setupHelper
-boolean
Captchaヘルパーをコントローラに自動的に追加するかどうか。デフォルトはtrue
です。$autoSetAnswer
-boolean
モデルにcaptchaの答えを自動的にセットするかどうか。デフォルトはtrue
です。
メソッド:
Captchableビヘイビア
設定:
$answerProperty
-string
captchaの答えが自動的にセットされるモデルのプロパティ名。デフォルトは'captchaAnswer'
です。$field
-string
バリデーションに使うフィールド名。the field to vlidate. デフォルトは'captcha'
です。$rule
-string
バリデーションに使うルールセット名。デフォルトは'captcha'
です。$convertKana
-boolean
true
のとき、全角の入力を半角に変換します。デフォルトはtrue
です。$trim
-mixed
入力の前後の空白を除去するかどうか、または空白の正規表現文字列。デフォルトはマルチバイトの空白の正規表現(PCRE)です。$required
-boolean
キャプチャの答えが必須かどうか。requireCaptcha()
を使って切り替えることも可能です。デフォルトはtrue
です。$setupValidation
-boolean
基本的なcaptchaのためのバリデーションセットをモデルにセットするかどうか。デフォルトはtrue
です。
メソッド:
最後に
最近はreCAPTCHAがよく使われるみたいですが、KCAPTCHAも昔からよく使われているもので、単純にcaptchaの機能を実装してしまいたい時に非常に便利です。
ご存知の方もいらっしゃるとは思いますが、Bakeryの記事で基本的なセットアップが載っています。:
Integrate CakePHP with Kcaptcha | The Bakery, Everything CakePHP
しかし、モデルのバリデーションでやるとなると、より多くのセットアップが必要になります。
これはそれら一連の動きを纏めたもので、非常に簡単にセットアップができるようになります。
APIリファレンスまで載せて、非常に長くなってしまいましたが、是非使ってみてください!
モデルの配列を管理するCollectionableプラグイン - Optionsビヘイビア
Collectionableプラグインとは
モデルのメソッドの引数やプロパティには、大きな配列が用いられるものがあります。
この管理を適当にすると、コピペの嵐になりがちです。
しかし毎度毎度、その管理の為のコードを書くのは億劫ですし、ロジックのミスにより予期しないバグの混入に陥ることがあります。
Collectionableプラグインは、そのような汎用的な配列の管理を提供するためのもので、配列の共通部分を減らし、かつ柔軟な切り替えを行うことを可能にします。
このプラグインはGitHubで公開しています。以下からダウンロードするかcloneするかsubmoduleとしてご利用ください。
また、英語ですがreadmeにはサンプルコードを載せてあります。
hiromi2424/Collectionable - GitHub
このプラグインには現在4つのビヘイビアが存在します。今回は、そのうち最もよく使うOptionsビヘイビアについて解説します。
Optionsビヘイビア
以前、モデルのfindオプションについて(特にPaginateに絞って)二つの記事を書きました。
Paginateオプションをモデルに移行する - 24時間CakePHP
paginateオプションをモデルに移行する(改善・修正版) - 24時間CakePHP
この記事に書いてあることを概ねビヘイビア化したものがOptionsビヘイビアとVirtualFieldsビヘイビアです。
ただし、この記事のコードよりも高機能です。
そしてOptionsビヘイビアは、findオプションを管理する役割を担います。
'options'属性、options()
オプションの指定の仕方は、二通りあります。
一つはfindのオプションに指定することです。
<?php $Model->find('all', array('options' => 'recent')); // recentオプションを指定する
もう一つはビヘイビアによって設定されるoptions
メソッドを呼ぶことです。
<?php $Model->options('recent'); // findオプションを返す
$options
プロパティ
モデルのオプションを管理するための配列を指定する場所は、モデルの$options
プロパティです。
$options
は名前がキー、値がfindオプションの単純な連想配列です。
以下のように記述します。
<?php class User extends AppModel { public $options = array( 'recent' => array( 'limit' => 10, 'conditions' => array( 'active' => true, ), ), ); }
単純な使い方
コントローラなどで単純にfindの結果を取得したり、paginationを行う際、直接呼び出すことになるでしょう。
例を挙げます。
<?php class UsersController extends AppController { public function index() { $this->paginate = $this->User->options('index'); $this->set('users', $this->paginate()); } public function recent() { $this->set('users', $this->User->find('all', array('options' => 'recent'))); } }
実践的な使い方
ここからが本題です。
単純な使い方ではそのメリットが分からないと思います。
コントローラで記述していたものがモデルに移っただけではありません。
Optionsビヘイビアには以下のような機能が備わっています。
- デフォルトのオプション
- 複数のオプションの指定(マージ)
- オプションにオプションを含めることができる(再帰的なマージ)
以下にReadmeから抜粋して、コメントを訳したものを提示します。
単純な投稿を扱うPostモデルがあるとします。
<?php class Post extends AppModel { var $hasMany = array('Comment'); var $hasOne = array('Status'); var $acsAs = array('Collectionable.options', 'Containable'); var $defaultOption = true; // デフォルトでこの値、'default'がデフォルトのオプションになる。もしくは文字列を指定してデフォルトのオプションを指定する var $options = array( 'default' => array( 'contain' => array( 'Comment', 'Status', ), 'limit' => 10, ), 'published' => array( 'condtiions' => array('Status.published' => true), ), 'recent' => array( 'order' => ('Post.updated DESC'), ), 'rss' => array( 'limit' => 15, ), 'unlimited' => array( 'limit' => null, ), 'index' => array( // このようにマージができるので、書き直す必要は無い 'options' => array( 'published', 'recent', ), ), ); }
これを以下のようにして使えます。
<?php class PostsController extends AppController { function index() { $this->paginate = $this->Post->options('index'); $this->set('posts', $this->paginate()); } function rss() { $this->paginate = $this->Post->options('index', 'rss'); // 実行時に複数オプションを指定できる $this->set('posts', $this->paginate()); } function all_in_one_page() { $posts = $this->Post->find('all', array('options' => array('index', 'unlimited'))); $this->set(compact('posts')); } }
より高度な使い方
全てのfindに対してデフォルトオプションを定義する
<?php class Post extends AppModel { var $acsAs = array('Collectionable.options', 'Containable'); public function find($type, $query = array()) { if (is_array($query) && empty($query['options'])) { $query['options'] = 'default'; } return parent::find($type, $query); } }
options()の挙動をオーバーライドする
<?php class Post extends AppModel { var $acsAs = array('Collectionable.options', 'Containable'); public function options() { $args = func_get_args(); // 何かする $result = call_user_func_array(array($this->Behaviors->Options, 'options'), $args); // 何かする return $result; } }
Containableが動かないなど
オプションによって挙動が変わる他のビヘイビアを同時に使う場合は、$actsAsの先頭にOptionsビヘイビアが来るようにしてください。
これはAppModelで定義するのが望ましいです。
まとめ
findオプションが煩雑になればなるほど、この機能は真価を発揮します。
また、find('list' or 'count')は使わない場合もありますが、findする時は常にこのオプションを使うようにすることはメンテナンス性を高めます。
例えば、作成日降順にしたり、無効フラグが立っていないことを後付けで全てに適用することがすぐにできるようになります。
もちろん、optionsをまったく定義しないモデルが存在してもそれはごく普通です。
例えば常に親テーブルが子テーブルを引っ張ってくるようなものなら、親テーブルのモデルに全てのオプションを記述すればいいだけです。
このビヘイビアを使うことによって、非常にシンプルな記述が可能になり、可読性の向上も望めるでしょう。
fatコントローラから脱却する第一歩でもあります。是非、使用を検討してみてください :D