cakephperの日記(CakePHP, Laravel, PHP)


継続的WebセキュリティテストサービスVAddyを始めました!

2015-04-27

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

2014-12-23

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

2014-12-02

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要素認証を導入しているところが多くなりました。Google、Dropbox、Zohoなど。

これらの多くは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()でそのまま利用できるため今回はそうしました。

2013-08-23

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で上記の指定をしておくか、CakePHPのMySQLデータソースを継承して、connect()メソッドをオーバライドして制御するなどすれば対応できるでしょう。


追記

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

2013-05-14

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と同じです)。

f:id:cakephper:20130514165225g:image

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

f:id:cakephper:20130514165224g:image


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

f:id:cakephper:20130514165223g:image

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

f:id:cakephper:20130514165222g:image


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


今回の移行では、現行のコードをCakePHP2.3で動かして動かない箇所をひたすら修正していく作業でした。それ以外のリファクタリングは行っていません。

色々と大変だったのですが、移行の話は下記の記事にまとまってます

「Cake Beer TalkでCake1から2への移行Tips100を発表しました」