移転しました。

字句を制限することで生PHPをテンプレート化する

Webを作るときにテンプレートエンジンがあると普通は開発が楽になると思う。しかし、PHPの場合はある特徴があるために、テンプレートエンジンが冗長に感じることがある。
それは、他の多くの言語では平で書くとプログラムとみなされるが、PHPの場合は平で書くと標準出力とみなされる点(<php...?>の中がプログラムになる)。要するに、PHPはそのものがテンプレートエンジンみたいな印象を受ける。
しかし、現実は甘くない。Smartyみたいなテンプレートエンジンを使っていないプロジェクトのコードだと、MVCのVの部分を見ても、物凄いコードが登場したりすることがある。これは良くない。
テンプレートエンジンを使うと何故Vの部分が綺麗になるのかという理由の一つに、プログラムコードを書くことが出来ないというのがあると思う(ちなみにSmartyには{php}があるのでその限りではないけれども)。テンプレート言語の範囲でしか記述できないという制約があるのでロジカルな部分が排除される。
そこで、ロジカルな部分を排除できれば元々テンプレートエンジンのような言語であるPHPのままでテンプレート化できるのではないのかなとか思った(ただしデザイナーがテンプレート書くような場合はまた話が違うとは思う)。
では、実際にはどうするのか。コードの字句レベルでの制限を加えればロジカルなコードを排除できないだろうかと考えて、禁止字句を書いている箇所を出力するコードを書いてみた(validator.php)。

<?php
if ($argc !== 3 || !is_file($argv[1]) || !is_file($argv[2])) {
  echo 'parameter error' . "\n";
  exit;
}

$config = parse_ini_file($argv[1]);
$source = file_get_contents($argv[2]);
$original = file($argv[2]);
$tokens = token_get_all($source);

foreach ($tokens as $token) {

  if (is_string($token)) {
    continue;
  }

  $tokenName = token_name($token[0]);
  $tokenSource = $token[1];
  $tokenLineNumber = $token[2];

  if (array_key_exists($tokenName, $config)
      && strtolower($config[$tokenName]) == 'off') {

    echo str_pad($tokenLineNumber, 5)
       . ' | '
       . str_pad($tokenName, 30)
       . ' | '
       . str_pad($tokenSource, 15)
       . ' | '
       . trim($original[$tokenLineNumber - 1])
       . "\n";
  }
}

コマンドラインから実行できて、例えば

$ php validator.php ./token.ini /usr/local/php/lib/php/HTTP/Request.php

のように実行すると、こんな出力が得られる。

311   | T_FOREACH                      | foreach         | foreach ($params as $key => $value) {
553   | T_FOREACH                      | foreach         | foreach ($value as $k => $v) {
577   | T_FOREACH                      | foreach         | foreach ($fileName as $name) {
902   | T_FOREACH                      | foreach         | foreach ($this->_requestHeaders as $name => $value) {
930   | T_FOREACH                      | foreach         | foreach ($flatData as $item) {
936   | T_FOREACH                      | foreach         | foreach ($this->_postFiles as $name => $value) {
943   | T_FOREACH                      | foreach         | foreach ($value['name'] as $key => $filename) {
990   | T_FOREACH                      | foreach         | foreach ($values as $k => $v) {
1062  | T_FOREACH                      | foreach         | foreach (array_keys($this->_listeners) as $id) {
1357  | T_FOREACH                      | foreach         | foreach (array_keys($this->_listeners) as $id) {

何行目に使ってはいけないトークンが書かれているよ、というような内容。
設定ファイル(token.ini)はこんな感じ(長いので一部抜粋)。

T_FOR = "On" ;for
T_FOREACH = "Off" ;foreach
T_FUNCTION = "On" ;関数
T_GLOBAL = "On" ;変数のスコープ

判り易いのでforeachのみをOffにしてる。因みに、設定ファイルの元ネタはPHP: パーサトークンの一覧 - Manualにあるトークンの全て。
ただ、現実問題としては、こういうツールがあるだけでは甘いと思う。開発者がテンプレートを書くときに自主的にツールでチェックするというような規約は弱い。間違っているとシンタックスエラーで動かない、という方が開発者は慣れているだろうし。
で、解決策としては何かしらフレームワークが必要になりそう。フレームワークの場合はテンプレートを読み込むロジックが一元化されているのが普通だと思うので、その部分でチェックしてエラーになったらログ吐いてexitみたいな。
あともう一つの問題はトークンの制御だけで本当にテンプレートが荒地にならないかどうかというところ。とはいえ、newとかclassとかfunctionとかevalとかrequireとか、Viewではいらないだろうというような字句を制限するだけでも、割とマシなテンプレートにはなりそうな気がする。