カテゴリー : CakePHP

CakePHP2.1 paginatorのsortを2カラム以上(複数)で実行する方法

CakePHPのページング機能を大変ありがたく利用させてもらっていますが、ビューでPaginator->sort()を使うとそのカラムだけの並び換えになります。一覧表を作成するとき、例えば日付順・顧客ID順などと表示したいときがあります。しかしいろいろと探して見ましたが上手い方法が見つかりませんでした。

以下の方法はコアコンポーネントを置き換える方法なのでお勧めできませんが、たった1行手を加えるだけで目的とする複数カラムの並び換えができるので、「どうしても!」と思う方は試してみて下さい。

Summary

コアコンポーネント「PaginatorComponent.php」のソース1行を書き換えます。アップデートで変更したソースが消えてしまわないように、「app/Controller/Component」にコピーしてそれに手を加えます。

並び換えはビューで指定されたカラムを優先し、コントローラの「$paginate」で指定したソート条件を加えるようにします。複数カラムのソートが必要なければ、「paginate()」をコールする前にパラメータを削除すればOKです。

How To

今回対象とするCakePHPのバージョンは2.1です。「PaginatorComponent.php」の「validateSort()」メソッドの中に、「named」パラメータで渡されたソート条件を検索条件にセットしている箇所が333行目にあります。

public function validateSort($object, $options, $whitelist = array()) {
    if (isset($options['sort'])) {
        $direction = null;
        if (isset($options['direction'])) {
            $direction = strtolower($options['direction']);
        }
        if ($direction != 'asc' && $direction != 'desc') {
            $direction = 'asc';
        }
        $options['order'] = array($options['sort'] => $direction);
    }

    if (!empty($whitelist) && isset($options['order']) && is_array($options['order'])) {
 :

変更は、

$option['order'] = array($options['sort'] => $direction);

$options['order'] = array_merge(array($options['sort'] => $direction), $options['order']);

とすればOKです。

 

CakePHP2.1 SecurityコンポーネントとAjax通信

Securityコンポーネントを利用するコントローラに、jQuery のAjax関数「$.post()」でアクションを実行しようとしても、しっかりとブラックホール行きとなり期待する処理ができない。どうするか、以下は自分で試してみての解決方法です。

先ず、セキュリティを回避してアクションを実行するには、以下のように「beforeFilter()」でSecurityのプロパティに値を設定すればいいようだ。

public function beforeFilter() {
    parent::beforeFilter();

    if ($this->params['action'] == アクション名) {
         $this->Security->csrfCheck = false;
         $this->Security->validatePost = false;
    }
}

しかしこのままの利用はセキュリティ上好ましくないので、Securityコンポーネントが出力するワンタイムトークンの利用を考えてみた。その方法は、ビュー内に出力されたトークンをjQueryで拾い上げ、サーバー側にPOSTデータが渡されたとき、セッションに保存されているトークンと比較する方法である。

以下はJavaScriptでトークンを拾い、Ajaxで送信する例。

var params = {
    'data[Comment][text]' : $("input[name='data[Comment][text]']").val(),
    'data[Token][key]' : $("input[name='data[_Token][key]']").val()
}

$.post('/comments/add', params, function(data) {
    alert(data);
});

Ajaxで送られたデータを受け取って処理する側「/comments/add」では、beforeFilter()でセキュリティを無効にしてadd()でトークンを確認すればよい。トークンはセッションデータに保存されているので、次のようにして読み出す。

$this->token = Session.read('_Token.key');

注意しなければならないのは、add()で読み出してもトークンは既に新しく書き直されているため、決して送ったトークンと一致する事はないことだ。そこで書き直される前、「beforeFilter()」で必ず読み出しておく必要がある。add()アクションでは次のように比較して確認する。

if ($this->request->data['Token']['key'] == $this->token)
 :

さてここで困った事に。それは、1回ならこのAjax通信は処理できる。が、続けての通信はセッションデータ内のトークンが書き直されているため、エラーになってしまうのである。

これを回避するための考え付いた方法が、Ajax通信する前の最初のアクションでセッションにトークンを保存しておき、比較はCakePHPが保存したトークンを使わず、自分で保存したトークンを使うのである。

これで取り敢えず解決した。

が、またまた問題である。

ブラウザで同じ画面を複数開くと、自分で保存したトークンの値が書き直されてしまうため、新しく開いた画面では処理可能だが、前の画面ではエラーになるのである。

皆さんならどのように解決するでしょうか?

自分の考え付いた方法はトークンの値として、Security.saltとセッションIDのハッシュ値を利用する方法です。これならログイン中は、複数画面で同じAjax処理が可能になるのではないかと。

 

CakePHP2.1 「Securityコンポーネントの怪」を避ける

やーっぱり、ハマりました。CakePHPのSecurityコンポーネントです。

Bakeして吐き出したコードを使ってもブラックホール行きで、毎度ゴミ箱に投げ捨てられている気分です。

少し分かったのは、フォーム出力されたデータ項目と入力されたデータ項目(これPOSTでなくPUTなんです)が異なると、ブラックホール行きになる事です。

で、何が問題だったかというと、入力されたデータがフォーム出力された項目より少なくなっていた、事でした。Form->input で「parent_id」と名付けた項目が何故か option要素のないselect要素で出力されたため、data[Model][parent_id]という入力データが無かったのでした。

回避策としてビューで

$this->Form->unlockField('Model.parent_id');

を実行しておけばOKです。

2012-03-08 以下ダメポなので一旦取り消し、要再調査とします。

しかし、そう言う事で毎度煩わされることなく、もう少し能動的にチェックする方法がないか考えてみました。それが次の方法です。

フォームのトークンとセッションのトークンを比較する

フォームデータの有無でチェックに引っかかることなくCSRFを回避すると共に、Securityコンポーネントを利用する事でページバックによる再入力も避けられます。

「beforeFilter()」で「$this->Security->validatePost = false;」として、フォームデータをチェックをしないようします。ところが、これをするとCSRFのチェックもしなくなってしまうので、次のように入力のワンタイムトークンのデータと、セッションに保存されたデータが等しいかチェックするメソッドを用意します。

protected function _checkToken() {
    if (isset($this->params['_Token']['key'])) {
        if ($this->params['_Token']['key'] != $this->Session->read('_Token.key')) {
            return false;
        }
    }
    return true;
}

アクションで _checkToken()メソッドを読み出せば確認できます。

AppControllerクラスで定義して、共通利用するようにすれば楽です。

(CakePHP 2.1 RC)

CakePHP2.1 ubuntu 11.10 コンソール Permission denied エラー

久しぶりにCakePHPに戻ってきた。早速2.1Betaをubuntu11.10サーバーにインストールして利用を始めた。で、Bakeしようとコマンドを実行したらエラーが発生。

ググって、修正して、エラーが出なくなって使えるようになったけど、モー訳が分からない状態なのでメモしておく事に。

CakePHPをWWWのディレクトリ下「/cake_2_1」に置いた状態で、カレントディレクトリを「cake_2_1」にして以下のコマンドをターミナルで実行する。

$ php lib/Cake/Console/cake.php
  :
Warning Error: SplFileInfo::openFile(.../app/tmp/cache/persistent/myapp_cake_core_file_map) : failed to open stream: Permission denied in .../lib/Cake/Cache/Engine/FileEngine.php on line 297
  :

とエラーが発生した。ググると以下のページで「Config/bootstrap.php」に、下のようにmaskパラメータを追加すると正常に動作するようになった、とあった。

http://cakephp.lighthouseapp.com/projects/42648/tickets/2172#ticket-2172-6

Cache::config('default', array('engine' => 'File', 'mask' => 0666));

そこにもあるように、maskの指定についてドキュメントに記述は見つからないが、取りあえず動くようになった。

特に出力されたファイルの読み書きモードが変わるわけではないようだが、maskパラメータを取り去って元に戻しておいた。それ以降は何事もなく動作する。

と思ったら、アプリケーションの実行で同様のエラーが発生。もう一度maskパラメータを付けて、persistentディレクトリのファイルを一旦削除してアクセスしたらエラーは発生しなかった。

モー訳分からず操作してます (´Д`;)。

追記:
maskパラメータなしだと、myapp_cake_core_file_mapファイルのオーナーグループはログインユーザーで出力され、maskパラメータを追加するとwww-dataで出力された。なので、www-dataにログインユーザーをグループ追加したが、それではエラー回避できなかった。う~ん?

追記2(120329):
app/tmp/logs/error.logへのエラー出力で、 /Cake/Log/Engine/FileLog.php が「file_put_contents()」を使ってエラーメッセージを追加する際、「Permission denied」が表示されメッセージの追加書き込みができないエラーが発生していた。ファイルパーミッションを0666で指定するもダメ。で、logsディレクトリのパーミッションを0764から0774に変更したところ、エラーが発生せずに動作するようになった。

CakePHP 外部PHPプログラムからライブラリのように使う方法

情報は数年前から散見できたが、使いたくなってきたので試してみた。驚くほど簡単に利用でき、今後のアプリケーション作りに活かせるかな、と云う事で備忘録となります。

先ずはソース。

<?php
	$_GET['url'] = 'favicon.ico';
	require_once = 'webroot/index.php';

	$dispatcher = new Dispatcher();
	$result = $dispatcher->dispatch('/posts/index', array('return'=>false));
	echo $result;

これで posts コントローラの index アクションを実行し、レンダリングされた結果が表示できる。

$_GET[‘url’]に文字列「favicon.ico」をセットすると、require_once で読み込んだ index.php の実行が dispatcher を動作せずに終了するようになっている。

さて、レンダリングは layout を含んでいるので、アクションのレンダリング結果だけを必要とする場合はどうするか?

dispatch()の2番目のパラメータで、「’bare’=>true」を与えればいいようだ(試した結果であって、ソース中身は確認してない)。

CakePHP の利用で大変ありがたいのがデータベースの処理だが、Model も簡単に利用できる。

<?php
	$_GET['url'] = 'favicon.ico';
	require_once = 'webroot/index.php';

	$postModel = ClassRegistry::init('Post');
	$post = $postModel->find('first');

	echo $post['Post']['title'] . '
' . $post['Post']['content'];

CakePHP はいよいよ 2.0ベータがリリースされたので、準備を怠らないようにしないと、と思うこの頃です。