MJ::Blog

日々のあれこれをテキトーに

エール@あけろすにっき

金曜日
5月 9,2008

PHP Framework Fight!
という何とも気になる闘いが催されている。

4月末におおーと気になってみていて
参加者エントリーに Akelosが無いなー
もし誰もエントリーしないならエントリーしてみようかなー

と、軽く考え

軽く考えた故にGW中は子供と遊んでいて忘却の彼方。

GW明け
そういやなんとかファイトあったよなーと思ってみてみたら
Akelosエントリー者発見。
つーか知らないフレームワークいっぱいだわ。

gegegenさんがAkelosでエントリーをしています。

gegegenさんガンガレ。
軽いエールを送ることにします。
ブログおっかけよっと。
ついでに自分の Ruby on Railsの勉強にもなるかもしれんわ

CakePHP 認証機能

日曜日
12月 9,2007

ログイン認証出来る機能がほしかったのだが標準で実装されているACLって認証機能じゃなかったのね orz
データベースなどのユーザーテーブル上のID/PWで認証できる機能を探していたところ、いろいろとモジュールが見つかり以下が参考になった。

とはいえ、個人で実装するにはいろいろと足りないところもあったりしたので、いいところを真似しつつ自作してみた。
それなりのレベルまで作れた感じな為公開してみることにした。不具合点などはご指摘下さい。
またご利用に際しては自己責任でお願いします。

使い方

コンポーネントとして動作。controllers/components にauth.phpとして配置。

まずはコンポーネントを利用するためにコントローラのはじめに以下のように設定

  var $components = array('Auth');

また、認証が必要なアクションを設定。
beforeFilter() メソッド中に認証が必要になるアクションを定義する方法を採用。

    function beforeFilter() {
        // 認証設定
        $this->Auth->requireAuth('userMenu', 'editUser');

        // 二重登録防止
        $this->Security->requireAuth('editUser');
    }

Securityコンポーネントと同様の方法で、認証が必要なアクションについて、requireAuth()の中にカンマ区切りでアクション名を羅列する方法としました。
検索して見つけたもろもろのモジュールでは、すべてに対して認証をしていたり、アクションの実行時に呼び出しを行ったりと面倒な方法をしているのが多かったのと、Securityコンポーネントと同じ方法で対応した方がわかりやすかったという理由で、こうしてみた。
Security->requireAuth()で二重投稿防止などをしている場合はその宣言より前に行う必要があります。

課題としては、無制限に認証をかける場合は allとか指定すると認証とか出来るといいのかもしれません。ま、今のところ必要ないから作ってないけどw

認証されているかをチェックする場合は、

  $this->Auth->checkAuth();

にてチェック。

ログアウトをする場合は、

  $this->Auth->logout();

とする感じで。

また、認証は ‘member’テーブルの ‘userid’ / ‘password’ を利用して行い、該当するユーザーが見つかった場合は ‘member’テーブル上のそのデータを認証ユーザーデータとして保存します。
実際は ‘Member’モデルで find()したユーザーデータが配列として保存されます。それらの値を取得する場合は、

  $value = $this->Auth->getAuthData('フィールド名');

member_name というフィールドの値を取りたい時は getAuthData(’member_name’) で取得。

コンポーネント側の設定

$authFormName:
ま、authFormNameという命名が正しいかどうかは微妙です。
ログインフォームで $html->input(’Login/userid’) などのように指定する際のモデル名(左では’Login’)を指定。

$authModelName:
認証で指定するモデル(テーブル)名。

$authLoginIdField:
ユーザーIDが格納されているフィールド名。 $authModelNameテーブルの $authLoginIdFieldがIDということになります。

$authPasswordField:
パスワードが格納されているフィールド名。 $authModelNameテーブルの $$authPasswordFieldがパスワードということになります。

$authLifeTime:
$authLifeTimeUnit:
上が認証有効期間、下がその単位。60分の場合は
$authLifeTime = 60;
$authLifeTimeUnit = ‘minutes’;
となる。strtotime() で比較するのでそこで認識できる単位を指定する。

$loginFormUrl:
ログインフォームURL。ログインが必要な場合はこのURLにリダイレクトされる。

$sessionName:
認証情報を保存するためのセッション名。ま、適当に。

ログインエラーとしては ID不正、パスワード不正、認証有効期間オーバーを用意しており、それらが
$this->Auth->getError() で取得出来るので、取得した内容をログインエラーとして表示できるんだけど、この辺がもうちょっとうまいこと実現できないかなーと思っているところ。
現状、たとえば loginForm() というアクションを用意して

    function loginForm() {
        $this->data[$this->Auth->authFormName] = $this->Auth->getLoginFormData();
        $this->set('errMsgs', $this->Auth->getError());
    }

のように errMsgsにエラー情報を渡してログイン画面を表示する必要があり、一つアクションに命令を各必要がある。
できればここは、書かずに処理したいんですよね。コンポーネント側だけで解決出来るといいかなーと。

書くべきところはこんな感じかしら?

ソース[auth.php]


class AuthComponent extends Object
{
    var $controller = true;
    var $components = array('RequestHandler', 'Session');

    /** 認証が必要なアクション名を格納する配列 */
    var $requireAuth = array();

    /** ログインフォームのフォーム名 */
    var $authFormName = 'Login';

    /** 認証で利用するモデル */
    var $authModelName = 'Member';
    var $authModel = null;
    var $authMember = null; 

    /** 認証モデルのログインID フィールド名 */
    var $authLoginIdField = 'login_id';

    /** 認証モデルのパスワード フィールド名 */
    var $authPasswordField = 'password';

    /** 認証有効期間 */
    var $authLifeTime = 60;
    /** 認証有効期間の単位 */
    var $authLifeTimeUnit = 'minutes';
    /** 認証有効時間(期間から算出) */
    var $authExpire;

    /** ログインフォームへのURL */
    var $loginFormUrl = '/members/loginForm';

    /** ログイン完了後にリダイレクトするURL */
    var $authLoginBackUrl;

    /** 認証で利用するセッション名 */
    var $sessionName = 'dragonquest';
    /**
     * セッションに引き渡し認証比較をするためのデータ
     *      array('Member' => array(),  ---> ログインしたユーザーの情報、authModelからfindして取得
     *            'Login'  => array(),  ---> ログインフォームでの ID/PWを作成するためのフォーム情報
     *            'errors'  => array(),  ---> ログインエラー情報
     *            'loginBackUrl' => String,   ---> 認証完了後に実行するアクション
     *            'expire' => String,   ---> ログインの有効時間 strtotime() の形式で
     *            '' => '',
     *           )
     */
    var $authData = array();
    var $authErrors = array();
    var $authDataErrorSessName = 'errors';
    var $authLoginBackUrlSessName = 'loginBackUrl';
    var $authExpireSessName    = 'expire';

    /* 認証エラー関係 */
    var $AUTH_NORMAL        = 0;
    var $AUTH_IS_NOT_LOGIN  = 1;
    var $AUTH_LOGIN_ID_ERR  = 2;
    var $AUTH_PASSWORD_ERR  = 3;
    var $AUTH_OVER_LIFETIME = 4;
    var $errMsgs = array(0  => '',
                         1  => 'ログインして下さい。',
                         2  => 'ユーザーIDが正しくありません。',
                         3  => 'パスワードが正しくありません。',
                         4  => 'ログイン有効時間をすぎたためログアウトしました。再度ログインして下さい。'
                        );

    /** StartUp */
    function startup(&$controller) {
        $this->controller =& $controller;

        // 初期処理
        $this->_init();

        // 認証処理を行うアクションの場合は処理の開始
        if (in_array($controller->action, $this->requireAuth)) {
            $this->start();
        } else {
            $this->setLoginBackUrl();
        }

        // authDataをセッションに書き出す
        $this->Session->write($this->sessionName, $this->authData);
    }

    /**
     * 初期処理
     */
    function _init() {
        //
        // 利用するモデルの定義
        // 認証データが格納されているモデルを定義
        //
        loadModel($this->authModelName);
        $className = $this->authModelName;
        $this->authModel =& new $className();

        // セッションに認証データがある場合は $authDataに値を設定
        if ($this->Session->check($this->sessionName)) {
            $this->authData = $this->Session->read($this->sessionName);
        }

        // 別名参照の作成
        // $this->authData[xxxxx] と書くのを省略したいだけw
        $this->authErrors =& $this->authData[$this->authDataErrorSessName];
        $this->authExpire =& $this->authData[$this->authExpireSessName];
        $this->authLoginBackUrl =& $this->authData[$this->authLoginBackUrlSessName];
    }

    /**
     * 認証制限するための action名を設定
     *
     * @params  array   引数で定義されたアクション名は認証制限をかける物とする
     */
    function requireAuth() {
        $this->requireAuth = func_get_args();
    }

    /**
     * 認証処理の開始
     */
    function start() {
        // 認証開始時にエラー情報破棄(ログインエラー時用にエラー情報をセッションで渡しているため)
        $this->authData[$this->authDataErrorSessName] = array();

        //
        // ログインフォームから入った場合は
        // ログイン処理を実行
        //
        if ($this->_isLoginExecute()) {
            if(!$this->login($this->controller->data[$this->authFormName][$this->authLoginIdField],
                            $this->controller->data[$this->authFormName][$this->authPasswordField])) {
                // ログイン失敗時
                $this->_redirectLoginForm();
            } else {
                // 別名参照の作成
                // ログイン成功時はログイン実行によって取得した会員情報を authDataに渡す
                $this->authData[$this->authModelName] =& $this->authMember[$this->authModelName];
                // ログイン有効時間を更新
                $this->setExpire();
            }
        } else {
            // 別名参照の作成
            // ログイン画面を経由していない場合は authMember にセッションに持っていた会員情報データを渡す
            $this->authMember[$this->authModelName] =& $this->authData[$this->authModelName];
        }

        //
        // 未ログイン時はログイン画面を表示
        //
        if  (!$this->checkAuth()){
            // ログイン完了後戻るべき URLをセット
            $this->setLoginBackUrl();

            // ログインエラーをセット
            $this->setError($this->AUTH_IS_NOT_LOGIN);
            $this->authData[$this->authFormName] = array();

            // ログインフォームへリダイレクト
            $this->_redirectLoginForm();
        }

        //
        // ログインしていても有効時間を過ぎていれば
        // やっぱりログイン画面を表示
        //
        if (!$this->checkExpire()) {
            $this->setError($this->AUTH_OVER_LIFETIME);
            $this->authData[$this->authFormName] = array();
            $this->_redirectLoginForm();
        }

        //
        // 問題なければ、認証時刻を記録
        //
        $this->setExpire();
    }

    /**
     * ログイン処理
     *
     * @param   String  $login_id
     * @param   String  $password
     * @return  Boolean True: ログイン成功 / False: ログイン失敗
     */
    function login($login_id, $password) {
        if (empty($login_id)) {
            $this->setError($this->AUTH_LOGIN_ID_ERR);
            return false;
        }

        //
        // ユーザーID / パスワードのチェック
        //
        $this->authMember =& $this->authModel->find(array($this->authLoginIdField => $login_id));
        if (empty($this->authMember)) {
            $this->setError($this->AUTH_LOGIN_ID_ERR);
            return false;
        }

        if ($this->authMember[$this->authModelName][$this->authPasswordField] != $password) {
            unset($this->authMember[$this->authModelName]);
            $this->authData[$this->authFormName][$this->authLoginIdField] = $login_id;
            $this->authData[$this->authFormName][$this->authPasswordField] = $password;

            // ログインエラーメッセージを表示してログイン画面を表示
            $this->setError($this->AUTH_PASSWORD_ERR);
            return false;
        }

        return true;
    }

    /**
     * ログアウト処理
     */
    function logout() {
        // セッションデータを削除する
        $this->Session->delete($this->sessionName);
    }

    /**
     * ログインされているかどうかのチェックを行う
     */
    function checkAuth() {
        return ($this->Session->check($this->sessionName) and !empty($this->authData[$this->authModelName]));
    }

    /**
     * ログイン有効時間をセットする
     * セットされた時間を過ぎるとログイン無効となる
     */
    function setExpire() {
        // authLifeTime (単位: authLifeTimeUnit) から有効時間を算出
        $this->authExpire = strtotime('+'.$this->authLifeTime.' '.$this->authLifeTimeUnit);
    }

    /**
     * ログイン時間が有効かどうかをチェックする
     *
     * @return  boolean     True: 有効なログイン
     */
    function checkExpire() {
        // authExpireが現在時刻いないであれば有効とする
        return (strtotime('now') < $this->authExpire);
    }

    /**
     * ログインエラーをセットする
     *
     * @param   Integer $errno  ログインエラーNo.
     */
    function setError($errno) {
        // ログインエラーNo. のメッセージも含めて
        // authErrors に対して格納
        $this->authErrors[$errno] = $this->errMsgs[$errno];
    }

    /**
     * ログインエラー情報を取得する
     *
     * @return  array    array(errno => errmsg, errno => errmsg, …)
     */
    function getError() {
        return $this->authErrors;
    }

    /**
     * ログインフォームから遷移したかどうか?
     *
     * @return  boolean     true: ログイン画面からの遷移
     */
    function _isLoginExecute() {
        // authModelName を名称とした POSTデータがわたる時はログインフォームからの遷移とする
        return !empty($this->controller->data[$this->authFormName]);
    }

    /**
     * ログイン完了後に戻るべきURLへリダイレクト
     */
    function _redirectLoginBack() {
        $this->Session->write($this->sessionName, $this->authData);
        $this->controller->redirect($this->authLoginBackUrl);
    }

    /**
     * ログイン画面へリダイレクトする
     * ログイン画面で表示するエラーメッセージ、入力フォームデータをセッションに格納し
     * リダイレクト
     */
    function _redirectLoginForm() {
        $this->Session->write($this->sessionName, $this->authData);
        $this->controller->redirect($this->loginFormUrl);
    }

    /*
     * Setter/Getter
     */
    function getAuthData($field = null) {
        if ($this->checkAuth()) {
            if ($field == null) {
                return $this->authData[$this->authModelName];
            } else {
                return $this->authData[$this->authModelName][$field];
            }
        } else {
            return false;
        }
    }
    function setAuthData($field, $value) {
        if ($this->checkAuth()) {
            $this->authData[$this->authModelName][$field] = $value;

            // authDataをセッションに書き出す
            $this->Session->write($this->sessionName, $this->authData);
        }
    }

    /**
     * フォームを中得する
     */
    function getLoginFormData() {
        if ($this->checkAuth()) {
            return $this->authData[$this->authFormName];
        } else {
            return false;
        }
    }

    /**
     * 実行中のURLを取得する
     * 実行したアクションに戻れるように
     *
     * @return String  アクションURL
     */
    function getLoginBackUrl() {
        if ($this->checkAuth()) {
            return $this->authData[$this->authLoginBackUrlSessName];
        } else {
            return false;
        }
    }

    /**
     * 実行中のURLを取得する
     * 実行したアクションに戻れるように
     *
     * @return String  アクションURL
     */
    function setLoginBackUrl() {
        if (getenv(’REQUEST_URI’) != $this->loginFormUrl) {
            // ログイン完了後戻るべき URLを現在のURLとして
            $this->authLoginBackUrl = getenv(’REQUEST_URI’);
        }
    }
}
月曜日
12月 3,2007

確認画面画面前の入力チェック。
CakePHPの入力チェックは Model部分にて クラス変数 $validate を定義することで save()メソッドによる更新時、validates() メソッドによる明示的な操作のいずれかで入力チェックを行うことができる。
入力チェックの方式としてはあらかじめ定義されている、VALID_NOT_EMPTY, VALID_NUMBER, VALID_EMAIL, VALID_YEAR で行えるのと、正規表現を利用することが出来る。定数 VALID_* もようは正規表現を定数定義してるだけだしね。

確認画面を表示する前の入力チェックでは validates() による明示的な入力チェックが基本となるか。
例えば、User モデルの入力チェックを行う場合コントローラー内で

  if($this->User->validates($this->data['User']) {
  // 無問題
  } else {
    $this->validateErrors($this-User);	// Viewで tagErrorMsgでメッセージを表示するための操作
    $this->render('フォーム');
  }

として、Viewでエラーメッセージを表示するように出来る View側では

<table class="registForm">
  <tr>
    <td>氏名</td><td><?php echo $html->input('User/name') ?><?php echo $html->tagErrorMsg('User/name', '氏名を入力して下さい'); ?></td>
</table>

とかしておくことで、name のバリデーションに引っかかった場合にのみ、’氏名を入力して下さい’ のメッセージが表示される。
この tagErrorMsg のエラーの吐き出しが、<div class=”error_message”></div> でエラーメッセージを表示してくれる。これによって CSSでerror_message を定義しておけば目立たせることなど出来る。個人的には <span> で囲ってほしかったわ。ここだけ <span> にしちゃいました。

また、入力エラーを目立たせたい場合以下のようにスタイルで class=”warning” などを使って、背景変えたり入力エラーの場所を目立たせることが多いので、

<table class="registForm">
  <tr class="worning">
    <td>氏名</td><td><?php echo $html->input('User/name') ?><?php echo $html->tagErrorMsg('User/name', '氏名を入力して下さい'); ?></td>
  </tr>
</table>

次のヘルパー追加して実現しました。

function tagErrorClass($field, $text) {
    $error = 1;
    $this->setFormTag($field);
    if ($error == $this->tagIsInvalid($this->model, $this->field)) {
        return sprintf('%s', is_array($text) ? (empty($text[$error - 1]) ? 'Error in field' : $text[$error - 1]) : $text);
    } else {
        return null;
    }
}

ま、tagErrorMsg を一部変えただけですね。呼び出す際は、

<table class="registForm">
  <tr class="<?php echo $hige->tagErrorClass('User/name', 'warning'); ?>">
    <td>氏名</td><td><?php echo $html->input('User/name') ?><?php echo $html->tagErrorMsg('User/name', '氏名を入力して下さい'); ?></td>
  </tr>
</table>

と言った感じで。

入力エラーの表示方法はどうも上記の方法しか用意されていないように思われる。探してみたところ。あったらすいません。
例えばフォームの上部にまとめて入力エラーの内容を表示するとか簡単にできそうもない。
このあたり、Validationに関しては結構使い勝手がもう一歩届かないというのが、現在使っているところでの感触だ。

日曜日
12月 2,2007

なんというか、
Mapleから CakePHPへ移行でもすんべ(地元の方言チックに)
とブログに書いてみたところ

開発者である kunitさんのブログにて、Maple の活動を再開しようと思っているとのこと。

さて、どうすんべw

現状の感触では Maple の方が好みに合っている部分も多かったので再開がんばってとエールを送ってみる。

せっかくなので今まで Maple-UserのMLにしか入っていなかったが
Maple-Devなんかにも入って見た。

月曜日
11月 26,2007

続きです。続けられました(1週間空いたけど)w
登録フォームの次は確認画面の表示です。厳密に言えば入力チェックが挟みますが、ひとまず確認画面ってことで。

  • 確認画面の表示
  • <input type=”hidden” …/> の自動生成

といった内容になっています。

確認画面、それは入力した内容を確認のため表示するという至極単純なページ。
単純なわりに、フォームと似て非なるものをつくるため工数がかさみます。

CakePHPではそこがちょっとは楽になるかなーと思ったのですが、特にこのありがちな流れを補完するような機能はないようですね。orz
とはいえ楽な部分もあります。
フォーム表示では、テキストボックスなどを表示するのに Htmlヘルパーを利用して、

  氏名: < ?php echo $html->input(’Member/name’); ?>
  フリガナ: < ?php echo $html->input(’Member/kana’); ?>

などとしますが、確認画面を表示する際はこのフォームの*.thtmlファイルをコピペして

  氏名: < ?php echo $html->tagValue(’Member/name’); ?>
  フリガナ: < ?php echo $html->tagValue(’Member/kana’); ?>

とすれば事足りる模様。これは置換すればいいだけで楽。 <input /> タグを強引に正規表現で置換してちょっと直すよりはずっといけてます。
プルダウンの表示などはこれだけでは済みませんが、これはこれで大分作業が楽ですね。

もう一つ必要な処理が、次の実行処理に必要なフォーム入力値の引き渡し。
今までは、<input type=”hidden” … /> を使ってたのですが、たくさんの入力項目があったりすると、すべてを記述していくのは面倒なもんです。
で、世の識者はどういう風にしているのかを調べてみると・・・

  • 確認画面表示時にセッションに保存し、実行時にセッションから引っ張る
  • 登録内容をひとまとめにして base64 などにエンコードして一つの hidden タグにまとめ、実行時にデコードして解析
  • 普通に hidden タグをゴリ書き

といったところのようです。どれも一長一短ですかね?セキュリティ面では2番目などがよいのかもしれません。セッションは何となく気分が悪いです(好みの問題)。
ま、自分は軽くオヤジはいっている古い人間なので、結局 3つめの方法に落ち着くことにした。
とはいえ、一つ一つ書くのはバカバカしいわけで考えてみたところ、POSTされた値から自動生成出来ますよね?(Ethnaではそういうメソッドがあるようでした)

というわけで独自にヘルパー作りました。内容は以下の通り

class HigeHelper extends Helper {
    var $helpers = array('Html');        // < -- Htmlヘルパー利用するため

    /**
     * 確認画面用の Hiddenタグを返す
     *
     * @param   String  $modelName   モデル名
     * @return  String
     */
    function hiddenVars($modelName) {
        $ret = "";
        foreach ($this->data[$modelName] as $key => $val) {
            $ret .= $this->Html->hidden(”$modelName/$key”).”n”;
        }
        return $ret;
    }
}

超簡単でした。ヘルパー名は適当。髭

はじめの氏名・フリガナの用にモデル名を Member としてフォームを生成している場合は、

  < ?php echo $hige->hiddenVars(’Member’); ?>

でOK。

たぶんこれでいけているはず。不具合あればご連絡下さい。またご利用は計画的に。

水曜日
11月 21,2007

いろいろと動かしては悩んでということを繰り返しています。CakePHP。
それなりに使えてきたのではと思います。

さて、CakePHP習得まずはじめに何をしたかというと
Webアプリの基本のフォーム送信ですね。会員登録でメール送信というアレ。
ちまたのCakePHPの解説とかホントか読むと、コントローラーで add() メソッド作って、 $this->data が送られてきたらフォームからの送信と見なして処理実行・・・というのが多く、フォーム > 確認 > 完了 という流れはあまりサンプルがなかった。

次のような感じですね。

class HigeContoroller extends AppController {
    略
    function add() {
        if (empty($this->data)) {
            // フォームデータからDB登録

        }
        ・・・・フォーム表示処理
    }
}

まあ、限られたスペースでの解説というのもありますが、上記の通りに作った場合、http://do.ma.in/hige/add というURL一つでフォーム表示・登録実行がなされます。二つの処理が同じURLだとログ解析時に同じ各処理が同じURLとして処理されるためあまりうまくない。フォームまで表示しても、その後の実行がないなどが分かれば、フォームに問題があるなどがわかるからね。従って、ここを分けるようにする。で、単なる会員登録ということで、registForm > registConf > registComplete としてみた。
毎回こういう英語で悩むのよね。Confと略すかConfirmとするか、Completeでいいの?とか。あとはキャメルケースでいいのか?アンダーバー(regist_formのように)で結んだ方がよいのか・・・などなど。誰かがこうせいというのを出してくれれば真似するのですがw

さて、フォーム表示ですが Htmlヘルパーを利用してがりがりと書いています。
DreamWeaverなどで見ようとするとうまくなさそうですが、結構便利ですね。
毎回 < ?php echo $html->なんちゃら(); ?> のように echo しないといけないのが何となくめんどくさい。とおもったら、AUTO_OUTPUT定数で変えられる模様。AUTO_OUTPUT = true にしてみたら、 $html->formTag がきちんと出力されないんですけど...

Htmlヘルパーでまずつまづいたのが、日付のプルダウン表示。
$html->dateTimeOptionTag(’Member/birth’, ‘YMD’, ‘NONE’); としてみたところ、月は英語表記 orz。つかえねー
調べてみたところいくつかやり方はあるようですが、ま、日本語日付ようのヘルパーメソッドを作るというのが基本のようで、あとは

  • もとの Htmlヘルパー(/cake/libs/view/helpers/html.php)を直接いじる
  • /app/views/helpers に上記 html.php をコピっていじる。/app/views/helpers/html.php を優先して呼ぶ模様(未検証)
  • 独自のヘルパーを作る

1番目の元をいじるのは嫌だし、2番目は元は残ってるとはいえ、CakePHPがバージョンアップして Htmlヘルパーが改善した時にめんどくさそう。ということで3番目を採用。

とりいそぎ、HtmlJp というヘルパー(名前適当)を作るということで、、、以下のようになりました。
ま、Htmlヘルパーの monthOptionTag, dateTimeOptionTag の一部を変更しただけです。

class HtmlJpHelper extends Helper {     // < -- HtmlHelper を extends してもよいのかしら?
    var $helpers = array('Html');	// Htmlヘルパー利用するので

    // 月のプルダウン表示
    function monthOptionTagJp($tagName, $value = null, $selected = null, $selectAttr = null, $optionAttr = null, $showEmpty = true) {
        if (empty($selected) && ($this->Html->tagValue($tagName))) {			// < -- このあたり $this->tagValue を $this->Html->tagValueに。
            $selected = date(’m', strtotime($this->Html->tagValue($tagName)));
        }
        $monthValue = empty($selected) ? ($showEmpty ? NULL : date(’m')) : $selected;
        $months = array(’01′ => ‘1′, ‘02′ => ‘2′, ‘03′ => ‘3′, ‘04′ => ‘4′, ‘05′ => ‘5′, ‘06′ => ‘6′, ‘07′ => ‘7′, ‘08′ => ‘8′, ‘09′ => ‘9′, ‘10′ => ‘10′, ‘11′ => ‘11′, ‘12′ => ‘12′);	   // < -- 英語表記やめ

        return $this->Html->selectTag($tagName . “_month”, $months, $monthValue, $selectAttr, $optionAttr, $showEmpty);
    }

    function dateTimeOptionTagJp($tagName, $dateFormat = ‘DMY’, $timeFormat = ‘12′, $selected = null, $selectAttr = null, $optionAttr = null, $showEmpty = true) {
        // —— 略 ——  上記と同じく  $this->なんちゃら は $this->Html->なんちゃらに・・・
        switch($dateFormat) {
            case ‘DMY’: // so uses the new selex
                $opt = $this->Html->dayOptionTag($tagName, null, $day, $selectDayAttr, $optionAttr, $showEmpty) . ‘日’ .
                $this->monthOptionTagJp($tagName, null, $month, $selectMonthAttr, $optionAttr, $showEmpty) . ‘月’ . $this->Html->yearOptionTag($tagName, null, null, null, $year, $selectYearAttr, $optionAttr, $showEmpty).’年’;
        // 以下同じように単純に、年月日というのを付けるだけ
}

といったところでしょうか。
あとは、コントローラーで忘れずに

  var $helpers = array('HtmlJp');

をつけて、Viewファイルで $htmlJp->dateTimeOptionTagJP() を呼び出す。
電話番号などいくつかのテキストボックスに分けるような時も一工夫必要かもしれません。

なんだか長くなってしまったので、続く、、、

CakePHPを習得中

月曜日
11月 19,2007

自分で構築する場合、PHPのフレームワークはMapleで落ち着こうかと思っていたのだけど、いろいろあって現在CakePHPに手を出している。

もともとMapleを選択したのは、自作していたフレームワークと設定ファイル等の扱い方・考え方が近かったのと、日本純正という理由からだった。Ethnaも合ったけどいまいち肌に合わなかった。ようは相性の問題である。
何げに使えていたのだが、最近の動向を見るにMaple周りの動きがほとんどないというのと、CakePHPの話題をあちこちで見るようになり「うーむ、つかいやすそうだなー」と感じたため、乗り換えるかーと思った次第である。

丁度、大分前に作ったシステムを作り直すという仕事が入っているため、そいつをぶっつけ本番でCakePHP化すんべ!!と現在鋭意制作中だったりする。

ここ1〜2週間他の仕事もこなしつつコーディングをしているのだが感触としては「うんそれなりに使いやすいね」といったところである。
バリデーションなんかはMapleの方が便利だったりするが、設定ファイルを使わないというのは結構楽なんだなーと実感した次第である。
現在のところ、登録フォーム〜完了までとかメールの送信とか認証機能とか、試行錯誤しつつCakePHPのソース見つつ調べつ実装してきたので、大分時間かかった感はあるが、以後はそんなに頭を悩ますこともないだろうから、実際の開発スピードを実感できるのはこれからかもしれない。
しかし、本格的なコーディングは1年ぶりくらいだったりするので、開発スピード結構落ちてるね。リハビリ、リハビリ。

今のところの実感としては

  • URLによって動作振り分けられるのは快適〜
  • コーディングする分量は確かに減るかもしれない
  • View にPHPのソースを詰め込んでいくと、DreamWeaverなどでソースを見る時にどうもなーと思う点。かといって、元々持っている便利なヘルパーを使わないのもどうもなーという感じ
  • Model の部分が個人的には微妙。findAll() とか確かに便利だが今一歩かゆいところに手が届かない〜

などなどいろいろと感じることがあったので、以後、CakePHPネタ続くかもしれません。

日曜日
6月 17,2007

ウノウラボにて
SmartyでRailsライクなレイアウトテンプレートを使う
というエントリーが上がったのを見て、ふと思い出したように投稿する。

ちなみに自分は Ruby on Railsは触ったことが無い。触ってみてーなーと思いつつ忙しいとか理由をつけて触っていない。
こういうときに歳を感じますね。

さて、上記レイアウトテンプレートと呼ばれているものだが、自分の場合は次の様に利用している。おそらく利用用途が異なるのだろうが、テンプレートファイルとしては header/body/footer と分割するのには意味があると思うわけだ。
layout.tpl に個別の contents.tplというのを内包するのは同じなのだが、自分の場合は次のようなことがやりたいわけである。header.tplから <html>タグをはじめる必要はない。

layout.tpl

layout.tplのソース的には

<html>
<head>
</head>
<body>
{include file="header.tpl"}

{include file="$view_file"}

{include file="footer.tpl"}
</body>
</html>

呼出側では

$view_file = "content.tpl"
$smarty->assign("view_file", $view_file);
$smarty->display("layout.tpl");

となる。

ということをいつもやる訳であるが、、、

そこで、Mapleである。Mapleなどのフレームワークを利用する場合は上記をもっと簡単に利用できなくては意味がない。
この layout.tpl であるが、同じWebアプリであっても表示する画面によってはベースとなるこのファイルを切り替える必要があるので、ベースとなる layout.tplとコンテンツとなる $view = “content.tpl” は maple.iniから設定出来る必要がある。そこで作ったのが Filter_BaseView(Filter_BaseView.class.php [zip圧縮])。

作ったと言っても、元々あった Filter_Viewのソースを一部変更しただけで、偉そうなことは言えません。

この Filter_BaseView.class.php を作成された webapp/components/maple/filter/ に配置する。
(ディレクトリがない場合は作成して)

maple.iniでは次のように指定をする

[BaseView]
baseView = "base.html"
success  = "content.html"

ちなみに、baseViewを定義しない場合は普通の Filter_Viewとおなじように機能される。

水曜日
4月 18,2007

最近ある程度の規模の開発を行う場合にフレームワークとして Maple を使用している。
元々は独自に OtokomaMVCと言う漢前なフレームワークを作って使っていたのだが、後付後付で機能を追加していった結果少し重くなってきたり、ごちゃごちゃしてしまったりしてしまったのだ。

本当は Symfonyが気になっているんだけど、PHP5専用と言うことで今のところ恩恵にあづかれず・・・
というわけで、Ethna, Mapleでどっちにしようかな〜と思い何となく性に合ってそうなのが Mapleだったので Mapleを使用している。

今回はそんな Mapleを使うにあたって個人的に気になったことを・・・

フレームワークを自作したときもそうだったけど、
基本的には指導するためのURLとして index.phpなど1つのファイルに集約させるのが動作上はシンプルだ。
Mapleも同じ方法で動作する。

http://domain/index.php?action=[ アクション名]

のようにパラメーター actionが変わるだけ。

この方法に欠点があるとすると、ログ解析時に全て index.phpと判断されてしまうところだ。
フォーム表示 → 確認画面 → 完了
と言う基本的な流れが全て index.phpで呼ばれてしまうため、どのアクションがどれだけ呼ばれたか?と言うのが分からなくなってしまう。
上記の流れを見たときに、フォームまで表示したが確認処理はしていない・確認処理まではしたが完了迄は至らないと言うのがログ解析で判断出来ると、問題を特定出来る可能性が高くなり完了というゴールへ到達する為の改善を行う為の指針となる。
これが分かると分からないとでは、結構大きく違うのだ。

そこで登場するのが Apacheの mod_rewrite。
.htaccessなどの設定で URLの書き換えを行うことが出来るのだ。
例えば、action.php → index.php?param=action のように書き換えられる。このときログ上は action.phpが呼ばれるように見えるのだ。
mod_rewriteを使えば、いたずらに呼出元のファイルを作ることなく index.phpの1つのファイルだけで処理が出来る。優れものなのである。

自分がよく行う方法としては以下の方法。

RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*).php?(.*)    index.php?action=$1&$2 [QSA,L]

Mapleはここのアクション名がなんだか長くなる性質があるので、必ずしも上記のみとは限らないのだが、、、
ま、考え方はこんな感じです。

が、しかし、これでは Mapleではうまくいかなかったのである。
例えば フォームからPOSTで test_action.php を呼出、テキストボックスなど幾つかの項目から値を送信する場合に問題があった(name=maji, email=maji@majima.net をPOSTで送信したとすると)。

URLとしては、 test_action.php が呼び出され。上記変数は POSTで送られるため URLには現れてこない。
これを、rewrite で操作をするとこの呼出を index.php?action=test_action に変換し POSTで送られたものはそのまま継続して送る形になる。 いわば GETとPOSTの混在である。

Mapleでは POSTとGETを同時に送ったときに問題があった。これに気がつくのに結構かかった。rewriteの設定を疑っていたので・・・

具体的には、Mapleの core/Request.class.php の Request() メソッドのところ。

if ($_SERVER["REQUEST_METHOD"] == "POST") {
    $request = $_POST;
} else {
    $request = $_GET;
}

の部分だ。

REQUEST_METHODが POSTの時は、Mapleでは POSTで送られた変数しか処理されないようになっていた。
いくら Rewriteで action=test_action とか送っても無駄だったわけである。
そこで、Rewriteでも機能出来るように次のように改変しちゃった。

if ($_SERVER["REQUEST_METHOD"] == "POST") {
    $request = $_POST + $_GET;
} else {
    $request = $_GET;
}

これでなんとか動くようになった。

もし、違う方法があるよ・・・と言うことでしたらコメントお願いします。

最近のコメント

  • mio: 月別アーカイブのカレンダーに、表示中の月のカレンダーを表示するところで、 悪戦苦闘していたところ、偶然このエントリーを...
  • ひろじ: ふたりともキマッてます! こどもって発表会とか楽しみにしますよね。 ウチもYAMAHAや保育園の発表会などは「あと何...
  • だるま: かわゆい! 奥方も 娘さんも! こちらのフラ教室も60歳で若手らしいw でも多くの人に愛されていいです...
  • MJ: そ、それは(;゜д゜) アンに「にんじん」というのと同様の効果が期待されます。...
  • ZODY(元YT^2): >赤毛のアン 俺のおかんも、はまっとりました。 還暦も間近っつうのに、「白毛のアン」と言ったら、口きいてくれな...

最近の投稿

Meta