PHPのフレームワーク再考
2006年からLaizを作り始めたわけだけれど、普通に利用する機能と実験的機能がごちゃ混ぜでフレームワークが肥大化している。
特に、PHP5.3用に変更したときにPHP4のコードをそのまま持ってきたり新しい仕組みを取り入れたりしてちょっと見づらいコードになった。
既に受託案件などで何度も使っているので主要機能についてはバグがあるとか動作がおかしいとかいうことはない。
でもいくつか使いにくい点というかもっと効率のいい方法があるんじゃないかと思うようになってきた。
変えたい部分は以下
- 設定を柔軟にしすぎた。結局使い方は2パターンくらいになる。
- 最初にPHPの全ファイルを読み込む
- 配列からオブジェクトへ
- PHP4の配列メインの構造からPHP5のオブジェクトメインの構造へ完全に変更できていない
で、結局これらを変更しようと思うと全体的な構成を変えないといけない。
もっと単純に、必要な機能だけを実装してシンプルでも使い勝手の良いものを作りたい。
フレームワークにやってほしいことって何だろうと改めて考えつつ書き出す。
この四つ、これだけあればいい。
それぞれ具体的に書くと
- フロントコントローラーとURLマッピング
- リクエスト変換
- 配列をオブジェクトに変換
- 全角を半角にするなどの共通処理
- フォーム入力チェック
- 一般的なバリデーション
- 共通処理
- フィルターなどと呼ばれるもの
- ログインやヘッダーフッターなどの共通処理をまとめる
このくらい。
おそらくURLマッピングとディレクトリ・ファイル構造がそのフレームワークの特徴の大部分を占めるのではないかと。
各種コンポーネントの取得方法や値の設定方法もそれぞれ特徴があるけれど、それは付随的な機能というかヘルパー的な、お助け機能。
まぁ大抵の場合はフレームワークの設定で任意のURLマッピングとディレクトリ構造にできるのだろうけれど。
それから必須のライブラリとして
- DB
- テンプレートエンジン
がある。
これはTsukiyoとYokazeまたはFlexyを多少変更して使おうかと。
あると嬉しいライブラリとしては
- メール送信
- ファイルアップロード
- 権限管理
- セッション管理
- トランザクション管理
- テスト補助
など。これらは既にあるものから選んで使うか100行くらいなら自分で書く。
多数のファイルを読み込んで様々な構成に対応するっていうのは無しの方向で。
最近のフレームワークってどうなってるんだろうと思って Zend Framework 2 のチュートリアルをやってみた。
http://framework.zend.com/manual/2.1/en/user-guide/skeleton-application.html
データベース1テーブルの操作するだけなのに、これは疲れる。
http://d.hatena.ne.jp/noopable/20121025
フレームワークの仕組みとしては、この辺を読むと使ってみたい気もする…。
http://www.nosenaoki.net/category/zf2/
こちらを読みつつ、使えるところを使いつつ薄いラッパーを書こうか考え中。
PHPで内部用ストリームAPI
内部用のAPIを作ろうと思ったときに、データが多い場合のことを考えると面倒だったりする。
offsetでページ切り替えするか?とか。
で、基本に返ってストリームから少しずつデータを取得すればいいんじゃないか、というC言語の初歩的なやつをPHPで。
put.php
<?php $first = true; for ($i = 0; $i < 100; $i++){ if (!$first) echo "\n"; else $first = false; $json = json_encode(array('data' => "$i\n")); echo $json; usleep(100000); ob_flush(); flush(); }
沢山のデータを表示する代わりにsleepして表示する。
get.php
<?php echo str_pad(' ', 1024); $stream = fopen('http://localhost/stream/put.php', 'r'); while(!feof($stream)){ $line = fgets($stream); $line = json_decode($line); var_dump($line); echo '<br>'; ob_flush(); flush(); }
str_padは確認のためにブラウザ上で逐次表示するため。
少しずつデータが流れてくるようならOK。
json_encodeは改行をエスケープするのでデータに改行があっても大丈夫。
これをIteratorで包めば10000件とかのデータも普通にDBをループするのと同じように処理できるんじゃないか?
PHPはリソースを解放しない方が良い?
今更ながら、目から鱗。
明示的にリソースを解放しない場合はHTTPコネクション切断後、つまり
リクエストシャットダウン時に解放されます。
明示的にリソースを解放した場合、HTTPコネクションがある状態、つま
りユーザーにページを送っている最中にリソースを解放する事になります。このため、PHPプログラミング(とくにWebプログラミング)ではリソース
[PHP-users 10171] Re: リソース変数の開放のタイミング
は明示的に解放しない方が良いです。例外は、多くのメモリなどを消費する
場合で、スクリプト実行中に解放した方が良い場合などです。
10年前なので、今はどうなのか分からないが…。
PHP5用のORM作った その2
昨日 *1 の続き。
https://github.com/nishimura/Tsukiyo
Joinの条件指定が欲しい、or検索が欲しい、テーブル名指定のjoinが欲しい、ということで機能追加。ほぼS2JDBC。
段々コードが見づらくなってきたかも。
OR検索
orやandが予約後なので、苦肉の策で変数代入。
<?php $or = Tsukiyo_Helper::$or; // または extract(Tsukiyo_Helper::$all); $iterator = $db->from('Item') ->sub($or()->eq(array('name' => 'a', 'opt' => 'option1')) ->like(array('name' => 'b')) )->isNotNull('opt') ->iterator(); // where ((name = 'a' and opt => 'option1') or (name like '%b%')) and opt is not null
$orはただのショートカットで、 new Tsukiyo_WhereTree('or') を返している。
条件指定join
join時に、プライマリキー以外の条件を付け加えたい場合は
<?php $iterator = $db->from('Item') ->outerJoin('SubItem', $or()->eq(array('subname' => 'foo')) ->isNull('subname')) ->iterator(); // item join sub_item on item.item_id = sub_item.item_id and // (subname = 'foo' or subname is null)
のようにする。
テーブル名指定join
これは、テーブルのリレーションが複数のパスを持つ場合に必要。関連の自動設定が出来ないので。
item <-- sub_item --> tag ↑ | |_________|
こういうテーブル構成のとき、
<?php $db->from('Item') ->outerJoin('SubItem') ->outerJoin('Tag')
とすると、Tagがどちらに結合されるか分からない。
どちらからもたどれるようにするのが理想なのかもしれないが、そういう特殊な構造は別途プログラムを書くということで。
<?php $db->from('Item') ->outerJoin('SubItem') ->outerJoin('SubItem.Tag')
TagはSubItemの方に結合しますよーという指定をする。
関連が一意に特定できるなら(上の図でいうと item <-- tag がないなら)outerJoin('Tag') でいい。
PHP5用のORM作った
O/Rマッピングライブラリを自作した。
ずっと前から自作ORMを作り替えたいと思っていて *1 、S2JDBCのようなものが作りたいからどうせならJavaやるかと思ってJavaをやり始め、やっぱりJavaよりPHPがいいと思ってPHPに戻ってきて、そうしたらまたORMに不満が出てきた。
作ってみたら三日くらいである程度形になった。ここ数年間作り替えたいと思い続けてきたもやもやは一体何だったんだってくらいにさくっとできたのでアップ。
https://github.com/nishimura/Tsukiyo
最近のPHPのフレームワークやORM事情はさっぱり分からない。
http://codezine.jp/article/detail/5858
ここをざっくり読んだけど今ひとつ心惹かれなかったので、何も参考にせずに作りたいように作った。
以下メモ程度に使い方。
まだ作り始めて一週間も経ってないので色々変わるかもしれない。
初期設定
DSNの設定、DBテーブル情報のファイルパス、DBのテーブル解析するかどうか、Voクラス名のprefixを設定する。
<?php require_once 'Tsukiyo/Db.php'; $db = new Tsukiyo_Db(); $db->setDsn('pgsql:host=localhost dbname=mydb user=myuser password=pass') ->setConfigFile('writable/config.ini'); ->setAutoConfig(true) ->setVoPrefix('Vo_');
必須なのは setDsnとsetConfigFile。setConfigFileは書き込み可能なパスを指定する。
実際使うときはSingletonクラスで包んだり共通設定ファイルのようなものにべた書きしたり。
SELECT文
<?php $item = $db->from('Item') ->eq('name' => 'Name 1') ->result(); $itemAndSubItems = $db->from('Item') ->join('SubItem') ->eq('Item.name' => 'Name 1') ->result(); $items = $db->from('Item') ->like('Item.name', 'a') ->order('itemId') ->iterator();
こんな感じ。
昔作ったORMだとjoinが出来なかったし条件の指定が微妙だったので、これらをごっそり変えたかった。
foreach ($itemAndSubItems->SubItem as $subItem) で結合先のデータを取ってこれる。
一対多なのか多対一なのかは自動判別。一対一は今後作る(多分)。
eq(他、ne、lt、le、などS2JDBC風)の引数はarray('プロパティ名' => 変数)の形式。テーブルが一つならarray('name' => 'foo')でいい。joinしてカラム名がかぶった場合はSQLを書く場合と同様にarray('Item.name' => 'foo')などと指定する必要がある。同一カラム名がないならテーブル名(Vo名)は不要。
複数取得はiterator()で。
ここで取得するイテレーターはforeach文が進むごとにデータベースにfetchしにいくのでちょっとクセがある。が、詰め替えするより処理は速いはず(測ってない)。
変数名の変換法則はDB側 sub_item.sub_item_id が SubItem->subItemId になるような感じ。
joinで自己結合はできない。同じテーブル名をjoinすることは不可(たぶんSQLエラーになる)。
UPDATE文
<?php $item->name = 'Item 2'; $db->save($item);
特に説明なし。insertじゃなくてupdateだと明示するならば $db->update($item) でもいい。
INSERT文
<?php $subItem = $db->generateVo('SubItem'); $subItem->itemId = $item->itemId; $subItem->name = 'Sub Name 1'; $db->save($subItem);
これも特に説明なし。新しい行のためのクラスはgenerateVoで生成、updateじゃなくてinsertと明示する場合は $db->insert($subItem) でもいい。
DELETE文
<?php $db->delete($subItem);
これもそのまま。$db->delete('SubItem', $subItem->subItemId) というようにテーブル名+プライマリキーでもいい。
ページャー
<?php require_once 'Tsukiyo/Pager.php'; $pager = new Tsukiyo_Pager($items, 10); $pagerHtml = $pager->getHtml();
簡易ページャー。
上の$itemsはイテレーター。イテレーターを渡すと、勝手にlimit、offsetを設定してくれる。コンストラクタ引数の10は一ページに表示する行数。ページャーのリンク表示数を変える場合は new Tsukiyo_Pager($items, 10, 5); のようにする。
その他
イテレーターの参照に注意。
<?php $iterator = $db->from('Goods')->iterator(); $sum = 0; foreach ($iterator as $goods){ $sum += $goods->price; } echo $sum;
これは想定通り合計が出る。
イテレーターが返すVoは同一のオブジェクトで中に参照を保持しているので
<?php $iterator = $db->from('Goods')->iterator(); $arr = array(); foreach ($iterator as $goods){ $arr[] = $goods; } $sum = 0; foreach ($arr as $goods){ $sum += $goods->price; } echo $sum;
これはダメ。配列に入れたオブジェクトは全て同じものを指す(具体的には最後の行)。
すぐに処理せずに詰め替えたい場合は
<?php $iterator = $db->from('Goods')->iterator()->setCloneVo(true); $arr = array(); foreach ($iterator as $goods){ $arr[] = $goods; } ...
setCloneVo(true)を呼び出す必要がある。
また、joinなどで二重以上のループがあると
<?php foreach ($items as $item){ echo $item->itemId; foreach ($item->SubItem as $subItem){ } echo $item->itemId; }
一回目のechoと二回目のechoは別のものになる。中のループが終わった段階でデータベースfetch用のカーソルは先に進んでいるのでVoの中身が変わっている。この場合もsetCloneVoが必要。
毎回指定するのがめんどくさそうなら初期設定に追加 or デフォルトの動作を変更するかもしれない。
また、一対多で二重以上のループにする場合は、一回のSQL文で全ての結合を取得するためにS2JDBCのようにorderでループ順に結果が並ぶようにしないといけない。
600行ほどのOrm.phpと120行ほどのIterator.phpがほとんど全てなので、ここを見ればだいたい書いてある。
今のところDBはPostgreSQLのみ対応。テーブル一覧やカラム一覧、外部結合の情報を取得するSQLを書けば他のDBでも動く、はず。
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を全体的に変更
いろんなPHP Webフレームワークで同じ画面を作ってみる (Laiz編)
元ネタはJavaなのでGWTでやろうと思ったけど画面遷移がないのでRPCを使ったとしてもうまく仕様に合わせられない。MVPで途中まで書いたがイマイチに終わり…代わりにPHPで。
仕様はこれ。
http://d.hatena.ne.jp/t1000leaf/20101125/1290612768
- 足し算プログラム
- 計算結果は、別画面に遷移し出力する。
- submit時にリクエストパラメータ名称で処理を切り分けるパターンを検証。
- バリデーションは、必須チェックと整数チェック。
- エラーメッセージは、一括で出力するパターンと、フィールド毎に出力するパターンを試す。
- DBアクセスはないよ。
PHP編は http://d.hatena.ne.jp/thatblue/20101202/1291310460 ここ。
下準備。
mkdir add; cd add git clone git://github.com/nishimura/laiz.git mkdir -p app/Base/Action app/Base/templates compiled htdocs chmod o+w compiled
<?php require_once '../laiz/Laiz.php'; Laiz::laze();
converterは別に要らないんだけど、フォームの入力値が複数ある場合はオブジェクトにした方が扱いやすいので変換。
app/Base/Action/Top.ini
[property] arg = [converter] arg = arrayToObject,Base_Arg
propertyはエラーの場合のデフォルト値設定用。
app/Base/Arg.php
<?php class Base_Arg { public $arg1; public $arg2; }
ついでに加算器。
app/Base/Adder.php
<?php class Base_Adder { public function add(Base_Arg $arg) { return $arg->arg1 + $arg->arg2; } }
次にアクションクラス。
入力フォームのアクションクラスは無し。
結果ページのアクションクラスは、リクエストパラメーターをオブジェクトで受け取って加算器クラスで計算する。
app/Base/Action/Result.php
<?php class Base_Action_Result { public $arg; public $result; public function act(Base_Adder $adder) { $this->result = $adder->add($this->arg); } }
Base_Adderはコンテナが自動で生成する。
バリデーションの全体設定。
app/Base/Action/Result.ini
[validator] errorMessage = "エラーがあります" errorMessageKey = error errorAction = Top file = Base/validator.ini [converter] arg = arrayToObject,Base_Arg
バリデーションの変数ごとの設定。
app/Base/validator.ini
[arg.arg1] required = "arg1 空欄エラー" digit = "arg1 数値エラー" [arg.arg2] required = "arg2 空欄エラー" digit = "arg2 数値エラー"
あとはHTML。
app/Base/templates/Top.html
<html> <head> <title>input</title> </head> <body> <div style="color:red"> {error}<br> {errorArgArg1}<br> {errorArgArg2} </div> <form action="Result"> arg1: <input name="arg[arg1]" type="text" value="{arg.arg1}"/><br/> arg2: <input name="arg[arg2]" type="text" value="{arg.arg2}"/><br/> <input type="submit" /> </form> </body> </html>
エラーメッセージの変数名はキャメルケース的な感じで。
app/Base/templates/Result.html
<html> <head> <title>result</title> </head> <body> result: {result} </body> </html>
最近Javaばっかりやっているけれど、こういう軽いページだとPHPは楽で良いと改めて思う。リモートからcatとviでちゃちゃっと書いた。