カテゴリー : CakePHP

CakePHP PEARライブラリの利用について

開発のどの時点かハッキリしないが、CakePHP 1.2で「vendor()」を利用してphpの外部プログラムが実行できないようになった。PEARのライブラリ群を利用する際、PEARへのインク ルードパスを一時的に設定して、望みのモジュールを読み出して利用するなど重宝していた。

共有サーバーでPEARを利用する際、使いたいモジュールだけコピーして使うのが、領域を節約できて良いように感じる。なので、ベースとなる PEAR.php を PEAR/PEAR.php とし、他のモジュールは、PEAR/XML/Rss.php のようにPEARディレクトリ下へコピーして使っている。

現在のCakePHP1.2で外部モジュールの読み出しは、「App::import()」を利用するようになったのだが、 「App::import(‘Vendor’, ‘pear.ini’)」で動作せず、いくつか試してみたがうまく動作させる方法がみつけられないでいる。

PEARへのインクルードパスを通すためのプログラムは、

<?php
define('PEAR_PATH', dirname(__FILE__) . DS . 'PEAR');
set_include_path(PEAR_PATH . PATH_SEPARATOR . get_include_path());
?>

で、ROOT/vendors/pear.ini.phpである。

さて、今どのようにしているかと言うと、アクションの先頭でrequire_onceを利用して、

require_once(ROOT . DS . 'vendors/pear.ini.php');
require_once('XML/Rss.php');

として、実行するようにした。

だったら、PEARをvendorsに置く必要もないか(^_^;)。

CakePHP AjaxでJSONデータの出力の仕方とcontent-typeについて

以前、Ajax通信で結果を出力する際XMLデータだったので、Content-Typeを「application/xml」か「text /xml」にしようと、RequestHandlerのメソッド(setContent)をいろいろ試したのだが、いつも「text/html */*」となり、なかなか思うように出力できなかった。あの時は、クライアント側で支障なく利用できたので、放り出してしまった。

今回、クライアント側のJavaScriptで扱いやすいように、JSONでデータを出力することにしたので、前回クリアできなかったハードルに再 挑戦することとなった。と言っても、CakePHPについは世界中で多くの情報が公開されているため、作業は検索と動作チェックだけだったけど (^_^;)。

お題は、「ユーザ番号をAjaxのGETで渡し、DBのユーザ情報をJSONで出力する」という、簡単なお話し。クライアント側については、参考に後で掲載する。自分のような気の短い人のために、サーバ側CakePHPによる出力のサンプルを先に見る(^_^)。

controllers/users_controller.php

var $components = array('RequestHandler');
var $helpers = array('Html', 'Form', 'Javascript');

function beforeFilter() {
  if ($this->RequestHandler->isAjax()) {
    if ($this->action == 'getUserAjax') {
      Configure::write('debug', 0);
      $this->RequestHandler->setContent('json');
      $this->RequestHandler->respondAs('application/json; charset=UTF-8');
    }
  }
}

function getUserAjax($userid=0) {
  if (!$this->RequestHandler->isAjax()) {
    $this->cakeError('error404');
  }
  $user = $this->User->findById(intval($userid));
  $this->set('user', $user['User']);
}

views/users/get_user_ajax.ctp

<?php echo $javascript->object($user); ?>

Content-Typeを正しく出力するためには、「respondAs()」を「beforeFilter()」の中で実行しないといけない。今まで散々アクションの中で試していたので、うまくいかなかった訳だ(T_T)。

ところで、ブラウザ側では「text/html」で受け取っても特に問題なく動作する。が、Content-Typeが正しくセットされていない場合の脆弱性についての話しもあるので、ご注意を。あまり詳しくないので、機会があればGoogle先生に尋ねてみようと思う(^_^;)。

さて、クライアント側ではどのようにAjaxでリクエストしているかだが、HTMLのセレクトボックスでvalueにユーザ番号を持ち、セレクト ボックスに名前が表示されるデータを用意しておく。これは、CakePHPのfind(‘list’)を利用すれば簡単だ。Javascriptには、 「jQuery」を利用する。

以下、HTMLのJavascriptのソース

<script type="text/javascirpt" src="js/jquery.js"></script>
<script type="text/javascript">
//<![CDATA[
$(document).ready(function() {
  $("#UserUserId").change(function() {
    $.get("/users/getUserAjax/"+$(this).val(), function(data) {
      var user = eval(data);
      $("#UserAddress").val(user.address);
          (その他必要な処理)
    });
  });
});
//]]>
</script>

と、こんな感じ。select要素にid=”UserUserId”の属性を持ち、option要素で指定されたユーザの名前を選択(changeイベント発生)したら、Ajax関数$.getにユーザ番号を付加したURLとコールバック関数を渡す。

Ajax通信が正常に終了すると、コールバック関数が実行されるので、その中でJSONのデータをevalを利用して、JavaScriptのオブ ジェクトに変換する。後は、HTML内のDOMにデータを埋め込む。例では、id=”UserAddress”を持つinput要素に読み出した住所を セットしている。

今回参考にさせていただいたサイトの記事です。ありがとうございました m(_ _)m。

CakePHP group byとinner joinでhasManyデータの件数を検索する

例えばusersテーブルとpostsテーブルがあり、「users hasMany posts」としたとき、ユーザの投稿したメッセージ件数の一覧表など作りたいことなど、普通にあると思う。そんなとき、usersテーブルとposts テーブルをJOINして、同じuser_idを持つpostsデータをGROUP BYでまとめて、count()で件数を引っ張り出す。SQLで書くと以下のような感じ。

SELECT User.id,User.name,count(Post.user_id) AS num FROM users AS User 
LEFT JOIN posts AS Post ON User.id=Post.user_id
GROUP BY User.id;

ここで、投稿を1度もしていないユーザを省略したい場合は、「LEFT JOIN」を「INNER JOIN」に変更すればよい。

さて、この自分が意図するSQLを、どのようにCakePHPを利用すれば、吐き出してくれるのだろうか?

以下のような方法で何とか解決したが、ちょっとトリッキーなやり方だな、と思う(ノ_-;)ハア…。

$this->User->unbindModel(array('hasMany'=>array('Post')));
$this->User->bindModel(array('belongsTo'=>array('Post'=>array(
  'className'=>'Post',
  'foreignKey'=>false,
  'conditions'=>'User.id=Post.user_id',
  'type'=>'inner',
  'fields'=>array('count(Post.user_id) as num')))));

$users=$this->User->find('all', array('group'=>'User.id'));

ここでのミソは、

  • belongsToを利用して「’type’=>’inner’」を利用する
  • JOINの条件は、’foreignKey’をfalseにし’conditions’に記述する
  • findするとき、’group’指定する

で、望みのSQLを吐き出してくれた。typeを省略すればLEFT JOINになるし、ユーザに条件を加えられるしで、なかなか使い勝手良いかも。modelに収めて隠ぺいすれば、controllerもスッキリ爽快気分になるに違いない(^o^)。

これと、group by句でposts.user_id指定したため、今日1日つぶした… orz。

以下、参考にしたサイトの記事です。ありがとうございました。

CakePHPのバージョンは、えーと、1.2.1_8004でした。

CakePHP ブラウザ終了でログアウト(セッションクッキーの有効期限)

CakePHPの認証機能「Auth」を利用しているが、ユーザー(ブラウザ)の操作とその結果表示で、どうにかならないかなぁ、と思っている点が2つある。

1つは、ログアウト後のブラウザの戻るボタン操作で、ログイン中の管理画面が表示されないようにできないか、ということ。

もう1つは、ログイン中にブラウザを終了し、その後ブラウザを起動してサイトの管理画面に再接続したとき、ログイン状態を破棄して再ログインを要求できないか、ということ。

前者は、まだいい解決法が見いだせていないが、糸口になりそうな点があったので最後に。

さて、ログイン時のセッションの有効期限だが、「config/core.php」の「Security.level」に「midium」をセット して、デフォルトで3時間20分となる。なので、ここを短くするように「Session.timeout」を変更する。すると、今度は管理画面でデータ入 力中にもかかわらず、セッションが切れてしまい不便な目にあう。ここを短縮することなく、ブラウザを終了したらセッションは切れて欲しいわけだ。

う~ん、どうしようか、お茶にしようか、で、おやつはCookieですよ。そうだ、クッキーの有効期限はどうなっているんだろう?と、FirefoxのCookieの表示で有効期限を見ると、7日も先になっておりますがな(ヾ(-_-;) オイオイ)。

ここの有効期限を変更すればいいんだね、ってことで試しにクッキーを削除してみる。

setcookie(Configure::read('Session.cookie'), '', 0);

お見事!これで、ログイン中にブラウザを終了すれば、セッションが継続されず再ログインになった。

「setcookie」コマンド使わなくても、プログラムを実行する際のセッション開始で、セッションクッキーの有効期限を「セッション終了時」に したいので、CakePHPコアモジュールに禁断のパッチを当てることにする。ファイルは「cake/libs/session.php」で、 「__initSession()」関数にあるメンバ変数「cookieLifeTime」への設定値を「0」にした。

case 'medium':
    //$this->cookieLifeTime = 7 * 86400;
    $this->cookieLifeTime = 0;

次に最初にあげた、ログアウト後にブラウザの戻るボタンで管理画面を表示しないようにする、方法について。

ブラウザのキャッシュに残っている情報を表示しているため、サーバー側から直接いじれないというのが、検索した結果の大方の意見。HTMLのheaderタグ下にmetaタグで、

  <meta http-equiv="Pragma" content="no-cache" />
  <meta http-equiv="Cache-control" content="no-cache" />
  <meta http-equiv="Expires" content="-1" />

など、いろいろと試すがどーにもならない。metaタグって役に立ってるの? オリャ(ノ-o-)ノ ┫;:・.

同じ「session.php」ソースの1つ下にある「__sessionStart()」関数を見ると、

  session_cache_limiter ("must-revalidate");
  session_start();

と、何やら怪しげな設定。で、「nocache」とパッチを当てると、戻るボタンで見事に表示しなくなった。

しか~し、問題が。フォーム入力で、JavaScriptの履歴戻る「history.back()」で戻ると、当然それまでの入力は何も残っていない(これは、リロードされているのかな?)。

ということで、このお話の続きはあるのか、ないのか……

補足:
CakePHP 1.2.0.7692-rc3で動作テスト

CakePHP Qdmailメール送信でメールが受信されない

Qdmailは、メール送信でBase64のコード変換や細かい作法を気することなく、日本語が文字化けしないようにメール送信を面倒見てくれる、大変優れたPHPライブラリである。国産で保守がしっかりされているので、安心して利用でき、使っている人も多いと思う。

最近サイトのお引っ越しでのリニューアルで、フォームメールのメール送信が、さくらインターネットへメールが届かない(受信しない)現象に出くわした。どうやら、原因が2つ3つ重なっていたようだ。

1.Return-PathがFrom送信者と同じでない

と、さくらインターネットのメールサーバは、メールを受け付けてくれないようである。QdmailでReturn-Pathを設定するには、 Qdmail(1.2.6b)の他、Qdsmtp(0.2.0a)を使う。Qdsmtpは、Qdmailから自動的にロードされるので、Qdmailの置 き場所(CakePHPではcontrollers/componentsディレクトリ)に、一緒に登録しておく。

    $param = array(
    'host' => 'xxx.net',
    'port' => 25,
    'from' => 'xxx@xxx.net'
);
$this->Qdmail->smtp(true);
$this->Qdmail->smtpServer($param);

2.SMTPサーバの認証が通っていない

メールが受信されるようになったので一安心していたのだが、次の日フォームメールを送ると、エラーが表示されるようになった。

QdSmtp error: Failure :status553 message:553 sorry, that domain isn't in my list of allowed rcpthosts (#5.7.1) on RCPT TO:<xxx@xxx.jp>
line -> 551
QdSmtp error: Error RCPT setting line -> 497
QdSmtp error: Error :status503 message:503 RCPT first (#5.5.1) on DATA
line -> 554
QdSmtp error: Error Data sending line -> 510
Qdmail error: Qdmail Version 1.2.6b ,PHP Version 4.3.9
Qdmail error: OS Linux ; PHP Version 4.3.9 ; Qdmail version 1.2.6b 
php.ini status: mb_language = Japanese ; mb_internal_encoding = UTF-8 ; mb_detect_order = ASCII,JIS,UTF-8,EUC-JP,SJIS 
Qdmail Status debug: 0, log: 0, errorlog: 0
Qdmail error: No send . Because SMTP mail method replied error line -> 2190
Qdmail error: Send Error line -> 2115

どうやらSMTPサーバに蹴られているようだ。認証は、POP Before SMTP方式なので、Qdsmtpへのパラメータを変更する。

$param = array(
    'host' => 'xxx.net',
    'port' => 25,
    'from' => 'xxx@xxx.net',
    'protocol' => 'POP_BEFORE',
    'pop_host' => 'xxx.net',
    'pop_user' => 'myname',
    'pop_pass' => 'mypass'
);

3.PHPセーフモードによるファイル書き込み禁止を回避する

ここに到達するのに、時間がかかった。

Qdsmtpはアクセス制御用のファイルを出力するので、まずは、出力をしないようにするためQdsmtpのメソッド「pop3UseFile()」を実行するのだが、「はて、Qdsmtpへの参照はどうするのだろう?」。

Qdmail関係のドキュメントを豊富に公開してくれているので、ここを見て助かったm(_ _)m。

$this->Qdmail->smtp(true);
$this->Qdmail->smtpServer($param);
$qdsmtp = & $this->Qdmail->smtpObject();
$qdsmtp->pop3UseFile(false);

さあ、これでOK。メールを送信するぞっ、っと…… orz ダメポ

うーん、さんざん悩んだ挙句、「pop3UseFile()」を使うのやめて、ディレクトリの許可属性を「rw」にしたところ、おーーーー、エラーが出ずに送信できた (T_T)。

で、「pop3TimeFilename()」で出力ファイルを決めて、めでたし、めでたしだったとさ。

POP Before SMTPは、クライアントのメーラーを利用するとしばらく送信が有効になるので、チェックするのに待ち時間が必要で貴重な休日の時間を一杯使ってしまったが、電子メールについていろいろ分かったこともあったので、良しとしよう(ムリヤリ納得)。