Category: PHP

PHPでwebアプリケーションを作るときよくあるのが、リクエストラインにパラメータを乗せて機能名やパラメータを載せる方法。

http://hoge.example.com/some_content/some_control.php?action=view&hoge=0&piyo=1

それに対して、リクエストパラメータを解析し共通のコントローラに渡すのがURLルーティング。前述のURLはこんな感じになるだろうか。

http://hoge.example.com/some_content/some_control/view/0/1/

実際に処理するのはフロントコントローラとかルータと呼ばれるクラスで、各種パラメータを解析して適切なコントローラを呼び出す(dispatch)のが仕事である。

kwappa開発室初の連載記事。まずは「Url Routing with PHP」(英語)という記事を元に、もっとも基本的なURLルータを設計してみる。

(さらに…)

前回include_pathを設定したとき「Smarty」の文字があったことからもわかるように、LolipopではSmartyが使用可能になっている。

まずはバージョンをチェック。Zendの技術情報コンテンツに「smartycheck.php」というコードが掲載されているので、LolipopのSmartyのバージョンを調べてみよう。

you can use Smarty(version 2.6.8).

2.6.8は2005年3月のリリース。最新版は2.6.19(2008年2月)なので、できれば新しいものを使いたい気分。

 

●オフィシャルからダウンロード

http://www.smarty.net/

●ディレクトリを作って展開

アーカイブの「libs」以下をすべてコピーし、必要なディレクトリを作る(名前は私の趣味。「templates_c」に違和感を感じるので。)

/smarty
  cache
  compile
  configs
  libs
  templates

cache,compile,configsはパーミションを0777に変更しておく。

●include_pathを設定

前回作った.htaccessを修正。

<IfModule mod_php4.c>
php_value include_path ".:/home/sites/lolipop.jp/users/[domain]-[your-subdomain]/web/lib/PEAR:/home/sites/lolipop.jp/users/[domain]-[your-subdomain]/web/smarty/libs:/usr/local/lib/php"
</IfModule>

●/smartyをふさぐ

Smarty関連のディレクトリは外から見えてもいいことはないので.htaccessでふさいでおく。

【/smarty/.htaccess】

Order allow,deny
Deny from all

●テストしてみる

【smarty_test.php】

<?php
define('SMARTY_ROOT', "/home/sites/lolipop.jp/users/[domain]-[your-subdomain]/web/smarty") ;
define('SMARTY_DIR',  SMARTY_ROOT . "/libs/") ;
require_once "Smarty.class.php" ;
$smarty = new Smarty() ;
$smarty->template_dir = SMARTY_ROOT . "/templates/" ;
$smarty->compile_dir  = SMARTY_ROOT . "/compile/" ;
$smarty->config_dir   = SMARTY_ROOT . "/configs/" ;
$smarty->cache_dir    = SMARTY_ROOT . "/cache/" ;
$smarty->assign("hoge", "piyo") ;
$smarty->display("smarty_test.tpl") ;
?>

【smarty_test.tpl】

smarty_test.tpl<br />
var 'hoge' = [{$hoge}] .

もちろんSmartyを使うページではベースクラスを作って使う。Smartyを使うと強制的に「俺フレームワーク」を書かされるのでいい勉強にもなると思う。

●リンク
本家:
http://www.smarty.net/

日本語ドキュメント:
http://www.smarty.net/manual/ja/

趣味でwebサイトを作るために、lolipopのサーバを借りている。安いわりにいろいろ使えるのだが、最初にわーっと作って満足してしまい、その後は放置が続いていた。仕事でweb作ってるってのにこれじゃいかん、ということで活用の道を探ってみる。

RSSを取得してごにょごにょするためにはPEAR::XML_RSSを使うと簡単なので、まずはPEARのインストールからやってみた。

●ディレクトリを作る

ftpでログイン、ルートに「lib」ディレクトリを作成。パーミションを0777にしておく。

●go-pear.phpを保存する

http://pear.php.net/go-pear

ソースコードがテキストとして落ちてくるので「対象をファイルに保存」、libディレクトリにアップロード。

/lib/go-pear.php

●ブラウザからインストール
http://[your-subdomain].[domain]/lib/go-pear.php

「NEXT>>」をクリック

「1. Installation prefix ($prefix)」をコピーしておく

/home/sites/lolipop.jp/users/[domain]-[your-subdomain]/web/lib

「Install」をクリック

●念のためbasic認証をかけておく

/lib以下は今後パッケージマネージャとして使うので、他人にいろいろ触れると問題がある。lolipopのツールを使ってbasic認証をかけておこう。

  1. lolipopユーザ専用ページにログイン
  2. 「WEBツール」>「アクセス制限」>「新規作成」
  3. http://[your-subdomain].[domain]/lib
  4. タイトル、アカウント、パスワードを設定する

●include_pathの設定

ルートに「.htaccess」を作成する。

<IfModule mod_php4.c>
php_value include_path ".:/home/sites/lolipop.jp/users/[domain]-[your-subdomain]/web/lib/PEAR:/usr/local/lib/php:/usr/local/lib/php/Smarty"
</IfModule>

赤字の部分がPEAR用に追加したもの、青字の部分がサーバのphp.iniで設定されているパス。

●ついでに

Lolipopのデフォルトでは若干問題のある(あるいは私の好みと異なる)設定が若干あるので、.htaccessで設定してしまう。上記IfModuleディレクティブに、以下の設定を追加しておく。

php_flag magic_quotes_gpc Off
php_flag short_open_tag Off
php_flag display_errors Off
php_flag log_errors On
php_value error_reporting 2047
php_value error_log /home/sites/lolipop.jp/users/[domain]-[your-subdomain]/web/log/php_error.log

開発中はdisplay_errors = On / log_errors = Offでもかまわないが、世間に公開したあとはdisplay_errorsはOffにしておこう。

●lolipop編の目標

PHP4で動く低機能フレームワークを作り、趣味のweb開発もさくさく進むようにする。

●参考にしたサイト

ロリポップでPEARをブラウザからインストールする! – ビキニ★プロ
http://d.hatena.ne.jp/bikinipro/20080124/1201185703

ブラウザからPEARをインストールする – ホリデープログラミング入門 – Yahoo!ブログ
http://blogs.yahoo.co.jp/nob_ll/46788965.html

zuzara : 格安サーバ・ロリポップを使い倒す
http://blog.zuzara.com/2006/07/23/100/

データベース接続パラメータ、テンプレート関数の色定義、絵文字などの各種変換テーブル、その他もろもろ…。webアプリケーションにはたくさんの設定項目(パラメータ)が存在することだろう。それらをどうやって定義・管理するかはなかなか悩ましい問題である。defineを山ほど書いてrequire_once?constだらけの定数クラス?配列が便利だからserializeしてファイルに書いておく?どれも一長一短だなぁ。

前代フレームワークの実装時、やはりこの問題に突き当たっていろいろと検討していたら、YAMLというデータフォーマットに行き当たった。テキストなので可読性が高く、パーサを使えば読み込んだ文字列を連想配列に落としてくれる。Rubyは1.8から標準でライブラリが添付らしい。むーうらやましい。

PHPではSyck(Rubyにも組み込まれているライブラリ)もしくはSpyc(ピュアPHPのライブラリ)から利用することになる。前代フレームワークでは導入の手軽さからSpycと、パフォーマンスのためにCacheYAMLというクラスを参考に書いたキャッシャクラスで運用している。

導入してみた感想は…「なぜもっと早くやらなかったのか?」。管理が簡単で、環境ごとの設定差分も同一ファイルで管理できる(メモリ効率は悪くなるが)。10万hit / day程度のモジュールに導入しても特に問題は発生していないので、パフォーマンスも問題なさそうだ。

kwappaではパフォーマンス向上のためにSyck + PEAR::Cache_Liteによる組み合わせも試してみる予定である。ConfigManagerクラスとしてまとまる予定なので、いずれエントリしようと思っている。

Spyc + CacheYAMLでの導入は簡単なのでコードは略。以下のリンクを参考にされたい。開発者・記事を書かれた方々には大変感謝している。

 

The Official YAML Web Site
http://yaml.org/

YAML – Wikipedia
http://ja.wikipedia.org/wiki/YAML

XMLの論考: YAMLはXMLに改良を加える
http://www.ibm.com/developerworks/jp/xml/library/x-matters23/

spyc: a simple php yaml class
http://spyc.sourceforge.net/

Do You PHP? – PHPでYAMLを扱う
http://www.doyouphp.jp/tips/tips_yaml.shtml

cl.pocari.org – PHP 用 YAML パーサ spyc の結果をキャッシュする方法
http://cl.pocari.org/2006-03-17-2.html

言語別 YAML用ライブラリ徹底解説:第4回 PHP編|gihyo.jp … 技術評論社
http://gihyo.jp/dev/serial/01/yaml_library/0004

Struts」とは、Javaの世界で一時期隆盛を極めたフレームワークである。私が関わっていた当時は1.2.9だったが、今はもう「Struts2」の名前でリリースされているらしい。MVCアーキテクチャを日本に広めた功績はあると思うが、コードの見通しの悪さには苦労させられた。

詳細各自で調べてもらうとして、その当時参考にさせてもらった手法をkwappaにも取り入れることにした。

Strutsはリクエストを受けると、URLにマッピングされたアクションクラスを呼び出して作業を行い、結果をJSPで表示する、という仕組みになっている。

このときアクションクラスが実際に処理を行うメソッドはデフォルトでは固定、というのが曲者で、たとえばひとつのフォームに「登録」「確認」「削除」のようなボタンをつけると、各ボタンそれぞれにアクションクラスを用意する必要があるのだ。

さすがにこれでは効率が悪いので、あるバージョンから「DispatchAction」という仕組みが用意された。リクエストパラメータに特定のキーを設定し、その値でアクションクラス内のメソッドを呼び分けてくれる。しかし、この方法でも「設定ファイルに書く」「GET / POSTパラメータに値を設定する」という2つのステップが必要になる。しかも名前をキーにメソッドを呼び出すので、ボタンのvalue要素に日本語が使えないという問題点もあった。それを解決するために「LookupDispatchAction」という拡張が施されたが、これもリソースファイルでマッピングを書く必要があるのでお手軽とは言い難い。

と、こんな仕組みの上でたくさんのDispatchを書いているとたいていのプログラマがめんどくさくなってくるのだが、その中のひとりが「こんな仕組み」を書いてくれた。「おおそうだこれだこれだ!」とばかりに当時活用させてもらったのみならず、今こうしてフレームワークの中でネタとして活用させていただいている。ありがたい話だ。

Kwappaではリクエストパラメータに「__[METHOD_NAME]__」という名前でなにか(なんでもいいがPHPでtrue判定される)値を送る。するとControllerのBaseClassがMethodを呼び分けてくれる、という仕組み。通常はsubmitボタンのname要素にメソッド名を記述する。DispatchAction / EasyDispatchActionではメソッドが見つからないとデフォルト(unspecified)メソッドを呼ぶようになっているが、KwappaではPHPがFatal Errorを出すに任せている。

【view】(template)

<form action="example.php" method="POST">
    <input type="text" name="hoge">
    <input type="submit" name="__foo__" value="call method foo" />
    <input type="submit" name="__bar__" value="call method bar" />
</form>

【controller】

<?php
class TestController extends KwappaController
{
    // メソッド名が指定されていない場合のデフォルト
    public function do_exec()
    {
        echo "function : default / value : {$_POST['hoge']}" ;
    }
    // [__foo__]ボタンを押した
    public function do_foo()
    {
        echo "function : foo / value : {$_POST['hoge']}" ;
    }
    // [__bar__]ボタンを押した
    public function do_bar()
    {
        echo "function : bar / value : {$_POST['hoge']}" ;
    }
}
?>

【super-class】

<?php
abstruct class KwappaController
{
    /**************** 前略 ****************/
    // デフォルト処理メソッドはオーバーライドさせる
    abstruct function do_exec() ;
    /**************** 中略 ****************/
    // リクエストパラメータによってdispatch
    private function dispatch()
    {
        $func_name = "exec" ;       // デフォルトの関数名
        foreach ($_POST as $key => $value)
        {
            if (preg_match("/__\w+__/", $key) && $value)
            {
                // input type="image" の場合、クリックした座標が入ってくるので対策
                $func_name = preg_replace("/_[xy]$/", "", $key) ;
                $func_name = "do_" . str_replace("__", "", $func_name) ;
                break ;
            }
        }
        // 抽出したメソッド名で実行して終了
        return $this->$func_name() ;
    }
    /**************** 後略 ****************/
}
?>

KwappaではURLルーティングを行わないつもりだった(前代もそうだった)。だが諸事情により方針転換したので、この辺まるっと書き直している最中である。Dispatchの仕組みは同じだが実際の処理はだいぶ違ったものになるので、今回は大まかな仕組みだけ紹介しておく。

余談ながら、input type=”image”の場合。

<form action="" method="POST">
    <input type="text" name="hoge" value="fuga" />
    <input type="image" src="button_img.png" name="__test__" />
</form>
<?php
    echo "<pre>" ;
    var_dump($_POST) ;
    echo "</pre>" ;
?>

【出力結果】

array(3) {
  ["hoge"]=>
  string(4) "fuga"
  ["__test___x"]=>
  string(2) "58"
  ["__test___y"]=>
  string(3) "114"
}

こんな感じで、ボタン画像のクリックされた座標が返ってくる。

携帯向けのサイトを作るのであれば、出力する文字コードは事実上Shift_JISしか選択肢がない。だがPostgreSQL(などRDB)も、PHPやテンプレートなどスクリプトファイルも、Shift_JISよりはEUC_JPのほうが都合がいい。

で、ずいぶん古いがこんな不具合がある。

【PostgreSQLウォッチ】第27回 SQLインジェクション脆弱性を修正,日本語ユーザーに大きな影響:ITpro

ので、データベース接続にclient encodingを指定しての自動変換も使いたくない(というか事実上使っちゃダメ)。

ということで、マルチバイト文字の流れとエンコーディングは以下のようにしている。

[SJIS] - クライアント
     ↓ 
[SJIS] - PHP (フィルタでSJIS->EUC変換)
     ↓
[EUC]  - PostgreSQL (store)
     ↓
[EUC]  - PostgreSQL (select)
     ↓
[EUC]  - PHP (mb_output_handlerがEUC->SJIS変換)
     ↓ 
[SJIS] - クライアント

php.iniの設定は以下のようになる。

output_buffering = On
output_handler = mb_output_handler
mbstring.language = Japanese
mbstring.internal_encoding = EUCJP-win
mbstring.http_input = pass
mbstring.http_output = SJIS-win
mbstring.encoding_translation = Off
mbstring.detect_order = auto

mbstring.http_inputはpassにしておくと入力がそのままPHPに渡るので、フィルタで料理してやる。autoを指定すると一見便利っぽいが、文字コードの自動判別は往々にしてしくじることがあるのでアテにしないほうがいいだろう。

なお、

mbstring.http_input = SJIS-win
mbstring.encoding_translation = On

にしておくと、入力を自動的に変換してくれる模様。kwappaでは他のコンテキストでもアプリが動いているサーバで開発していたので、ここでは設定せずフィルタで変換する方式にした。この辺は好みと環境でいいのではないだろうか。…厳密な検証はしてないのだが。

 

携帯なので入出力に絵文字が混じるため、SJIS,EUC-JPではなくSJIS-win,EUCJP-winを指定する。絵文字は機種依存文字(外字)エリアのコードを使うらしいのでこの設定が必要になる模様。

私が担当する以前はPHP内部がSJISだった(client encodingを使用していた)ため、既存の絵文字関連ルーチンはすべてSJIS用に作られている。そのため絵文字処理のEUC版を実装しなくてはならなくなった。近い将来行うので、形になったらエントリしようと思う。

まずは3キャリアに向けてXHTMLを出力する準備。

ポイントはDoCoMo端末にXHTMを出力する際、default_mimetypeを出力してやること。

ini_set関数の前に少しでも出力があるとtext/htmlが送られてしまうので注意。コントローラ部分で「echo “hoge=[{$hoge}]\n” ;」とかデバッグプリントしちゃうことがよくあると思うけど、それがあるとXHTMLとして解釈してくれなくなる。

au端末はヘッダに不備があると「このページは表示できません」というダイアログが出てレンダリングを中止してしまうため、開発中のプレビューには向いていない。

ついでにキャッシュの生存期間を出力している。kwappaは更新頻度の高いコンテンツで使われているので、キャッシュされることにはデメリットのほうが大きくなってしまうので。

ということでsmartyのヘッダ出力関数と、キャリア別に用意したヘッダ部分のテンプレートを掲載する。

<?php
/**
*  Smarty plugin {kwappa_header}
*
*  usage: {kwappa_header [title=$title]}
*
*  キャリア別ヘッダの出力
*
*/
// 実際はもっとcommonな場所でdefineしておくが…。
define("KWAPPA_TERM_PC"0) ;  // PCで見た場合(テスト用)
define("KWAPPA_TERM_DCM", 1) ;  // DoCoMo
define("KWAPPA_TERM_AU"2) ;  // au
define("KWAPPA_TERM_SB"3) ;  // SoftBank
function smarty_function_kwappa_header($value, &$smarty)
{
    // あらかじめコントローラでキャリア種別を取得しておく
    $term_type = $smarty->get_template_vars('term_type') ;
   
    // キャリアごとにヘッダを出力
    switch ($term_type)
    {
        case KWAPPA_TERM_DCM :
        {
            ini_set("default_mimetype", "application/xhtml+xml") ;
            $header_tpl = "inc/header_dcm.tpl" ;
            break ;
        }
        case KWAPPA_TERM_AU :
        {
            header("Cache-Control: no-cache") ;
            header('Expires: Sun, 10 Jan 1990 01:01:01 GMT');
            header('Pragma: no-cache');
            $header_tpl = "inc/header_au.tpl" ;
            break ;
        }
        case KWAPPA_TERM_SB :
        {
            header("Cache-Control: no-cache") ;
            $header_tpl = "inc/header_sb.tpl" ;
            break ;
        }
        default :
        {
            $header_tpl = "inc/header_dcm.tpl" ;
            break ;
        }
    }
    // タイトルをassign
    if (isset($value['title']))
    {
        $smarty->assign("kwappa_page_title", $value['title']) ;
    }
    // ヘッダを表示
    $smarty->display($header_tpl) ;
}
?>

header_dcm.tpl

<?xml version="1.0" encoding="Shift_JIS" ?>
<!DOCTYPE html PUBLIC "-//i-mode group (ja)//DTD XHTML i-XHTML(Locale/Ver.=ja/1.1) 1.0//EN" "i-xhtml_4ja_10.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type"       content="application/xhtml+xml; charset=Shift_JIS" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<title>{$kwappa_page_title}</title>
</head>
<body>

header_au.tpl

<?xml version="1.0" encoding="Shift_JIS" ?>
<!DOCTYPE html PUBLIC "-//OPENWAVE//DTD XHTML 1.0//EN" "http://www.openwave.com/DTD/xhtml-basic.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type"       content="application/xhtml+xml; charset=Shift_JIS" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<title>{$kwappa_page_title}</title>
</head>
<body>
header_sb.tpl 
<?xml version="1.0" encoding="Shift_JIS" ?>
<!DOCTYPE html PUBLIC "-//J-PHONE//DTD XHTML Basic 1.0 Plus//EN" "xhtml-basic10-plus.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type"       content="application/xhtml+xml; charset=Shift_JIS" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<title>{$kwappa_page_title}</title>
</head>
<body>