PHPの小規模用テンプレートエンジンを作った
今までPHPで何かを作るときはLaizを使っていたわけだけれども、レンタルサーバー的な構成だと使いづらい。
数ページのフォームとかだとPATH_INFOに対応するためにApache設定ファイルにAliasMatchを書いたりフレームワーク設定ファイルを書いたりするのが面倒になる。それにPHP5.3用に作っていたのでPHP5.2以下では動かない。
ちょっとしたフォームを作ときに、HTMLに直接PHPのコードを書くよりは少し便利な程度の単純なテンプレートエンジンが欲しくなった。
60行で作るPHP用テンプレートエンジンのようなものを作った。
最初は同じようにHTMLにPHPを書くつもりでやっていたけど、オブジェクトを使うとエディタ上で -> が閉じタグに解釈されて色がバグったりする。なのでHTMLを解析するバージョンも作った。
それからLaizのデータベース接続部分のコードを単体でも使えるように移動した。
テンプレートエンジン+α
https://github.com/nishimura/Yokaze
ここに置いた。
Template.php は70行なのでここにも貼っとく。このファイルだけ単体でも使える。
<?php class Yokaze_Template { protected $templateDir = 'template'; protected $cacheDir = 'cache'; private $ext = 'html'; private $vars; public function __construct($templateDir = null, $cacheDir = null) { if ($templateDir) $this->templateDir = $templateDir; if ($cacheDir) $this->cacheDir; $this->vars = new StdClass(); } public function setExtension($ext) { $this->ext = $ext; } public function setVars($vars) { $this->vars = $vars; } public function show($vars = null) { if ($vars === null) $vars = $this->vars; $file = basename($_SERVER['SCRIPT_FILENAME'], '.php') . '.' . $this->ext; $tmplFile = $this->templateDir . '/' . $file; $cacheFile = $this->cacheDir . '/' . $file; $this->compile($tmplFile, $cacheFile); $this->showCache($cacheFile, $vars); } protected function compile($tmplFile, $cacheFile) { if (file_exists($cacheFile) && filemtime($tmplFile) <= filemtime($cacheFile)){ return; } $tmpl = file_get_contents($tmplFile); // include feature $incPattern = '|{include:([[:alnum:]/]+\.html)}|'; if (preg_match($incPattern, $tmpl)){ $incReplace = '<?php $this->compile(\'' . $this->templateDir . '/$1\', \'' . $this->cacheDir . '/$1\'' . ');' . ' include \'' . $this->cacheDir . '/' . '$1\'; ?>'; $tmpl = preg_replace($incPattern, $incReplace, $tmpl); } // simple variables $tmpl = preg_replace('/(\{[[:alnum:]]+)\.([[:alnum:]]+(:[a-z]+)?\})/', '$1->$2', $tmpl); $tmpl = preg_replace('/\{([[:alnum:]_>-]*):h\}/', '<?php echo $$1; ?>', $tmpl); $tmpl = preg_replace('/\{([[:alnum:]_>-]*):b\}/', '<?php echo nl2br(htmlspecialchars($$1)); ?>', $tmpl); $tmpl = preg_replace('/\{([[:alnum:]_>-]*)\}/', '<?php echo htmlspecialchars($$1); ?>', $tmpl); file_put_contents($cacheFile, $tmpl); } private function showCache($__cacheFile__, $__vars__) { foreach ($__vars__ as $k => $v) $$k = $v; include $__cacheFile__; } }
特に目新しいことはしていない。Flexyに慣れているのでそんな感じに。
テンプレートに{var}と書けば htmlspecialchars($var)を表示、{var:h}と書けばそのまま表示、{var:b}と書けばnl2brを挟む。オブジェクトは{obj.prop}と書く。ただし多段{obj.obj.prop}には対応していない。正規表現がすぐ思い付かなかったので。
あと{include:header.html}などと書くとheader.htmlを読み込む。
ディレクトリの階層には非対応。というか階層分けが必要な規模なら別のちゃんとしたフレームワークを使うと思う。
使い方の例。
public_html/ cache/ .htaccess config/ Yokaze/ .htaccess config.php template/ .htaccess form.html result.html form.php result.php
.htaccess にはアクセス拒否の設定を書く。またはcacheやtemplateディレクトリをpublic_htmlの外に出してもいい。
config.php
<?php ini_set('display_errors', 'On'); error_reporting(E_ALL | E_STRICT); require_once 'Yokaze/Template.php'; $t = new Yokaze_Template(); require_once 'Yokaze/Session.php'; $s = new Yokaze_Session(); require_once 'Yokaze/Request.php'; $r = new Yokaze_Request(); $r->message = $s->remove('message');
SessionやRequestは使わなくてもいいけど、使った方が簡単に書ける。
form.php
<?php require_once 'config/config.php'; $r->subject = ''; $r->body = ''; $r->initPost(); function success(){ global $r, $s; $s->set('message', '送信しました。'); // ここでDB登録、メール送信など $r->redirect('result.php'); } if ($r->isPost()){ if (!$r->get('subject')) $r->message = 'タイトルを入力してください。'; else if (!$r->get('body')) $r->message = '本文を入力してください。'; else success(); } $t->show($r);
$r->subject = ''; などが若干めんどくさいが、データベースを使う場合はより簡単になる。それは後述。
$r->initPost() は$_POSTをRequestの変数として展開する。$r->initPost($obj) と書くと $objの中に展開する。
form.html
<!doctype> <html> <head> <meta charset="UTF-8"> <title>フォーム</title> </head> <body> <div style="color:red;">{message}</div> <form action="form.php" method="POST"> タイトル:<input type="text" name="subject" value="{subject}"> <br> 本文:<textarea name="body">{body}</textarea> <br> <input type="submit" value="送信"> </form> </body> </html>
result.php
<?php require_once 'config/config.php'; $t->show($r);
特に何もしない。その場合でもphpは必須。
result.html
<!doctype> <html> <head> <meta charset="UTF-8"> <title>送信完了</title> </head> <body> <div style="color:red;">{message}</div> </body> </html>
O/Rマッピングツール + α
O/RマッピングツールはLaizから移植した。
https://github.com/nishimura/YokazeDb
プライマリキーはテーブルごとに一つ必要。名前は何でも良い。joinには非対応。その場合はsqlファイルに書く。
PostgreSQLとSqliteに対応。Sqliteの動作確認はあまりしていない。テーブルのメタ情報を取得する関数を書けば他のDBでも対応可能。
2005年からつぎはぎしているのでコードは汚いがそれは置いておく。メソッドや引数も綺麗じゃないので気が向いたらs2jdbc風に書き直すつもり。
さきほどのフォームから送信されたデータをDBに登録するように変更する。
まずデータベースを以下のように作る。
config/tables.sql
create table form( form_id serial primary key, subject text not null, body text not null, created_at timestamp not null );
createdb yokaze_sample psql -f config/tables.sql yokaze_sample
<?php // ・・・省略 require_once 'YokazeDb/Factory.php'; $factory = new YokazeDb_Factory(); $dsn = 'pgsql:host=localhost;dbname=yokaze_sample;user=user;password=pass'; $factory->setDsn($dsn);
form.php を変更する。
<?php require_once 'config/config.php'; $orm = $factory->create('form'); $form = $orm->createVo(); $r->initPost($form); $r->form = $form; function success(){ global $r, $s, $form, $orm; if (!$orm->save($form)){ $r->message = '送信に失敗しました。'; return; } $s->set('message', '送信しました。'); $r->redirect('result.php'); } if ($r->isPost()){ if (!$form->subject) $r->message = 'タイトルを入力してください。'; else if (!$form->body) $r->message = '本文を入力してください。'; else success(); } $t->show($r);
$orm->createVo()はテーブル情報を読み取ってカラムに対応するフィールドを持ったクラスを生成する。
insert時にcreatedAtの値が自動挿入される。更新時はupdatedAtが自動更新される。
form.html の方は{subject}などを{form.subject}のように変更する。
登録されたデータを確認するページを作る。
list.php
<?php require_once 'config/config.php'; $r->forms = $factory->create('forms')->setOptions(array('order' => 'createdAt')); require_once 'config/YokazeDb/Pager.php'; $pager = new YokazeDb_Pager($r->forms, 10); $r->pager = $pager->getHtml(); $t->show($r);
一覧表示用のイテレーター取得は1行で書ける。
表示を整形したい場合はSPLのIteratorIteratorを被せる。
このイテレーターはHTMLを表示してforeachに差し掛かったときに初めてデータベースにfetchしにいくので、HTMLに echo $PDOStatement->fetch()['name']; と書く場合に比べてそれほど遅くならないはず。
list.html
<!doctype> <html> <head> <meta charset="UTF-8"> <title>入力値一覧</title> </head> <body> <table> <tr> <th>タイトル</th> <th>登録時刻</th> <th>本文</th> </tr> <?php foreach ($forms as $form): ?> <tr> <td>{form.subject}</td> <td>{form.createdAt}</td> <td>{form.body:b}</td> </tr> <?php endforeach ?> </table> <div> {pager:h} </div> </body> </html>
foreachはPHPで書く。
HTMLにPHPでforeachを書くのが嫌だという場合は new Yokaze_Template() の代わりに new Yokaze_Parser() する。
そうすると
<table> <tr> <th>タイトル</th> <th>登録時刻</th> <th>本文</th> </tr> <tr class="loop:forms:form"> <td>{form.subject}</td> <td>{form.createdAt}</td> <td>{form.body:b}</td> </tr> </table>
classやstyle、その他適当な属性値にloopを書ける。
TODOなど
- Request は $r->message = 'foo'; なのに、Session は $s->set('message', 'foo'); になっている。どちらかに統一する。
- タグの属性にif文が書ける。書式は class="if:$var" や style="if:$iterator.count()" や foo="if:$item.status===1" など。ちょっと見にくいので書き方を考える。
- メール送信用ユーティリティクラスを追加する。
- エラー表示をオフにしてエラーをメールで送信するユーティリティ的関数を書く。
- ORMを全体的に変更