CakePHP2のPaginateでCASE式などを使ったフィールドを指定してソートする方法

CakePHP2で、CASEやCONCATなどを使ってテーブルに存在しないフィールド名でソートを行う場合、例えば下記のようにすると思います。

<?php
$params = [
  'fields' => ['CASE WHEN User.age >= 20 then 1 else 0 END AS adult'],
  'order'  => ['adult DESC']
];
find('all', $params);


通常のfindであれば問題ないですが、ページングのorderでこのフィールドを指定しても効いてくれません。
このような場合は、モデルのバーチャルフィールドを利用します。

<?php
//コントローラの中でUserモデルを利用
$this->User->virtualFields['adult'] = 'CASE WHEN User.age >= 20 then 1 else 0 END';
$this->Paginator->settings = ['order' => ['User.adult' => 'desc']];


参考:
http://book.cakephp.org/2.0/ja/models/virtual-fields.html
"バーチャルフィールドは find 時に普通のフィールドと同じように振舞うため、Controller::paginate() はバーチャルフィールドでもソートすることができます。"

http://stackoverflow.com/questions/21160171/implement-order-by-case-with-paginator-in-cakephp

CakePHP3のfind結果はdebug関数で見ると良い

CakePHP3を触り始めています。Cake3からfind()の結果がオブジェクトになりました。
Cake2までは配列だったのでpr関数で見ても問題なかったのですが、オブジェクトになるとprは辛くなります。

CakePHP3からはdebug関数を使うと下記のようにリレーション先のデータも確認できるようになります。debug関数はcakephpが標準で用意しているグローバル関数です。CakePHP2にもあります。

debug( $this->Users->find()->contain(['Bookmarks'])->all() );

Users hasMany Bookmarksの状態でfindすると、下記のようなデータがdebug()を通して確認できます。
Usersオブジェクトの中にitemsフィールドがあり、その中身が下記になります。
リレーション先のエンティティオブジェクトがbookmarksフィールドに入っていて、その値も確認できます。

(int) 0 => object(App\Model\Entity\User) {
'properties' => [
        'id' => (int) 1,
        'email' => 'xxxxxxx@gmail.com',
        'password' => 'xxxx',
        'created' => object(Cake\I18n\Time) {
                'time' => '2014-12-23T06:48:20+0000',
                'timezone' => 'UTC',
                'fixedNowTime' => false                         
        },
        'updated' => null,
        'bookmarks' => [
                (int) 0 => object(App\Model\Entity\Bookmark) {
                        'properties' => [
                        'id' => (int) 1,
                        'user_id' => (int) 1,
                        'title' => 'aaa',
                        'description' => 'aaa',
                        'url' => '',
                        'created' => object(Cake\I18n\Time) {
                                'time' => '2014-12-23T07:11:21+0000',
                                'timezone' => 'UTC',
                                'fixedNowTime' => false
                        },
                        'updated' => null
                                ],
                        'dirty' => [],
                        'original' => [],
                        'virtual' => [],
                        'errors' => [],
                        'repository' => 'Bookmarks'

                }
]
],

Debug用のクラス(Cake\Error\Debugger)など他にも用意されていて、ドキュメントも下記にあります。
http://book.cakephp.org/3.0/en/development/debugging.html

CakePHP2にTwilio SMSを使った2要素認証機能を追加

CakePHPアドベントカレンダー2014の2日目の記事です。12/1に @K1LoWさんが突然アドベントを始めたので乗り遅れないように2日目を担当します。まだアドベントカレンダーは空いているので皆様も是非。

そうそう、12/10にCakePHP2実践入門が電子書籍として発売されます。紙の本を出してから2年ぐらい経ちますが、これからCakePHP2を始める方には良いかなと思います。
http://gihyo.jp/news/nr/2014/12/0101


CakePHPのAuthコンポーネントを使うと簡単に認証機能が追加できます。詳細は日本語チュートリアルをご覧ください。

一般的なIDとパスワードを使った認証ですと、emailなどの共通したIDのシステムでパスワード使い回しのユーザがいる場合に標的になる可能性があります。これを防ぐために最近では2要素認証を導入しているところが多くなりました。GoogleDropboxZohoなど。
これらの多くはID/PW認証が完了した後に、SMSを送信してトークンを入力させて認証を行います。SMSが届く携帯電話などが必要です。

SMSの送信はTwilioを使うと簡単に実現できます。TwilioはPHP SDKを提供しているのでPHPに組み込むのも簡単です。具体的には下記のコードになります。

<?php
$client = new Services_Twilio($account_sid, $auth_token);
$message = $client->account->messages->sendMessage(
  $from_number, //Twilioで取得した電話番号(SMSはUS電話番号を使う必要があります)
  '+819011112222', //SMSの送信先携帯番号
  "sms_token: " . $token //SMSのメッセージ
);

あとはこのコードをAuthコンポーネントを使ったログイン用のコントローラアクションに挟むだけです。Authコンポーネント自体を変更したり継承してオーバライドする必要もありません。
CakePHPの2要素認証のサンプルをGithubに上げました。詳細はそちらをご覧ください。
https://github.com/ichikaway/cakephp-2FactorAuth-SMS


メインの処理は下記のコミットになります。
https://github.com/ichikaway/cakephp-2FactorAuth-SMS/commit/33f0ce45bbb47c95eafc5211cb0eb33cf6ba09e8

注意点としては、$this->Auth->login()でID/PW認証をしてしまうと、自動的に認証が通ったセッションが生成されてしまうため、いくらSMSトークン入力画面を挟んでも、ログイン後URLに直接遷移されてしまうと無意味になります。
ですので、このサンプルでは、

 $this->Auth->identify($this->request, $this->response);

を使って最初にID/PWのマッチングのみをしています。

ID/PWがマッチした場合にのみ、SMSを送信してトークン入力画面を表示します。トークンが送信されてユーザが入力し、トークンが一致すると、hiddenで引き継いだID/PWを使って$this->Auth->login()を実行してログイン成功のセッションを作成しています。
別にhiddenで引き継がなくてもSessionに入れて引き継いでも良いと思います。hiddenにしておくと$this->Auth->login()でそのまま利用できるため今回はそうしました。

CakePHPのfind()で取得したデータが全てstring型になるのを、DBのカラムの型に合わせてint型で値を取得する方法(mysql)


CakePHP2からはPDOを使ってDBアクセスするようになりました。PDO(mysql)では、デフォルト設定でデータをfetchするとint型のカラムでもstring型として結果が返ってきます。CakePHPもこの影響を受けており、jsonデータなどに変換する際や、型を厳密に扱いたい場合に影響がます。(CakePHP1では、PDOを使っていませんがintカラムはstringで返ってきます)


この問題を解決するには、PHP5.3以上の環境でPDOのPDO::ATTR_EMULATE_PREPARESをオフにすれば良いです。PDOがmysqlndドライバを利用することが前提なのですが、PHP5.4からはデフォルトでmysqlndドライバが利用されるので大丈夫です。今回はPHP5.4の環境で検証しました。

PHP5.3ではPDOがmysqlndドライバを利用するためにコンパイルオプションを指定するか、パッケージインストールでphp-mysqlndを入れるなどが必要です。
参考 http://stackoverflow.com/questions/13159518/how-to-enable-mysqlnd-for-php


mysqlndについてや、PDOとの関係などは下記の資料がまとまっています。
http://blog.layer8.sh/ja/2013/03/12/php-mysql-pdo-mysqlnd/


さて、CakePHP2でPDOのフラグオプションにPDO::ATTR_EMULATE_PREPARES = falseを指定すれば良いのですが、mysqlデータソースを見るとアプリケーション側から指定する仕組みがないため、コントローラの中などでPDOインスタンスを取得してセットすることにしましょう。
例えば、jsonを返すコントローラのアクションの中で下記のようにすればfindの結果は型が正しい状態で返ってきます。

<?php
$pdo = $this->Post->getDatasource()->getConnection();
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES,FALSE);
var_dump($this->Post->find());

やっていることは、Postモデルを使って(モデルは何でも良い)、getConnection()でPDOインスタンスを取得します。
あとはPDOクラスのsetAttribute()メソッドでPDO::ATTR_EMULATE_PREPARESにfalseを指定しています。


もしアプリケーション全体でこのオプションを有効にしたい場合は、AppModelやAppControllerで上記の指定をしておくか、CakePHPMySQLデータソースを継承して、connect()メソッドをオーバライドして制御するなどすれば対応できるでしょう。

追記

akiyan先生からのご指摘で、タイトルが紛らわしいと気付いたので変更しました。ありがとうございました。

CakePHP1.2から2.3にアップデートしたらサーバの負荷が半減した

あるプロジェクトでCakePHP1.2を使っていたのですが、そろそろサポート期間も終わりそうな気がしたのでCakePHP2.3の最新版にアップデートしました。
サイトは月に数百万PVぐらいの規模で、DBテーブル数は80ぐらい、それに加えてViewテーブルやストアドプロシージャを使ってます。これを3年ぐらい前のさくらの専用サーバ1台でさばいてます。(Xeon 2コア、メモリ4G、Apache, mod_php(5.3), MySQL構成)


CakePHP2からモデルのレイジーローディングなども入ってコア自体も効率化されたため、パフォーマンスは上がるだろうなとは思ってました。
実際に本番サーバにデプロイしたところ、CPUのロードアベレージが半分ほどになりました。


週単位のグラフを見ると、リリース前はピークが200ぐらい、平均100ぐらい(100はtopコマンドなどで見るロードアベレージ1と同じです)。

リリース後には、ピークが100、平均50ぐらいになってます。ちょっと縦軸の幅が違うので分かりにくいかもしれませんが。


1日単位のグラフで見ると、リリース前は日中の負荷は100〜200の間ぐらいでしたが、

リリース後の日中の負荷は100以下になってます


CakePHP1と2のパフォーマンス比較はHelloWorld的なもので1.5倍ぐらいパフォーマンスが良くなるというのは計測で分かってました。実アプリのコードにすると複雑さが上がるのでCakeコアの効率化による恩恵は大きいことが分かりました。


今回の移行では、現行のコードをCakePHP2.3で動かして動かない箇所をひたすら修正していく作業でした。それ以外のリファクタリングは行っていません。
色々と大変だったのですが、移行の話は下記の記事にまとまってます
「Cake Beer TalkでCake1から2への移行Tips100を発表しました」

(2013/4/28) CakePHPのPaginatorコンポーネントに脆弱性、すぐにバージョンアップを!

CakePHP1.2, 1.3, 2.xのPaginatorコンポーネント脆弱性があり、最悪はSQLインジェクションが起こる可能性があるとのこと。
http://bakery.cakephp.org/articles/markstory/2013/04/28/security_release_-_cakephp_1_2_12_1_3_16_2_2_8_and_2_3_4


早急にアップデートすることをおすすめします。
各バージョンの最新版はgitのブランチから取得するか、下記から該当バージョンのzip/tarファイルをダウンロード可能です。
https://github.com/cakephp/cakephp/tags


探すのが面倒な人は、下記から直接ダウンロードを。
https://github.com/cakephp/cakephp/archive/2.3.4.zip
https://github.com/cakephp/cakephp/archive/2.2.8.zip
https://github.com/cakephp/cakephp/archive/1.3.16.zip
https://github.com/cakephp/cakephp/archive/1.2.12.zip

CakePHP2.0、2.1はメンテナンス終了っぽいですね。今後は2.2以上にした方がよいです。


いやはや、ゴールデンウィークにすごいのが出てしまった。。。
10連休にして海外とか行かなくて良かったな orz

CakePHPのレールの外し方(CakePHP勉強会 uluru 2013/04/19)

株式会社うるるで開催された、第1回CakePHP勉強会で発表してきました。
今回のお題は、「エンジニアとデザイナーの協業」でした。


デザイナー向けというのは、実は3回目ぐらいなので何を話そうかなと悩んでました。悩んだ末に、プログラマがどこまでデザイナに優しいCakePHPにカスタマイズできるかやってみようというストーリにしました。



一番言いたかったことは、「ルールは最後に自分たちで決める、決める上で何が正しいかはプロジェクトごとに違うので、世の中で正しいとか、これがCake流だとか、ベストプラクティスとか言ってるのに流されすぎない方が良い」ということでした。
CakePHPでも積極的にヘルパー使わないとか、レイアウトファイル使わないとか、Apache側のmod_rewriteでがんばると世界が広がるとかを、コードベースで紹介しました。特にmod_rewriteを使うという発想でディレクトリ構成を自由にしておくというのは、なんとなくアイディアはあったけど実際やったことがなかったので、今回の発表に合わせて検証して大丈夫だというのが分かり良かったです。


株式会社うるるでのCakePHP勉強会はシリーズもので、第2回、3回と既にお題が決まっております

2013年
4月 #1 エンジニアとデザイナーの協業
6月 #2 プラグイン, PHP Matsuriについて
8月 #3 テスト, CakeFestについて


株式会社うるる社員のイベント運営も素晴らしいですし、ビール飲めますし、開場も熱気があって面白いので、次回も楽しみにしています。


ちなみに株式会社うるるでは、CakePHPで自社サービスを作ってるそうで、絶賛WEBエンジニア募集中のようです。面接の際にはぜひ、cakephperのブログ見たと言ってもらえれば、「へー」って言ってもらえると思います!