はじめに
Phalcon には Volt というテンプレートエンジンが同梱されています.構文としては twig と同様のものを採用しているみたいです.
Volt: テンプレートエンジン — Phalcon 1.3.1 ドキュメント
このページには Extensions という項目がありますがここはわりと何も書いてないに等しいのでちょっと調べた内容を書いておきます.
Volt の Extension とは
Volt はそのまま twig の構文として書かれたものを ブラウザに html として返すのではなく,一回 php ファイルにコンパイルします.
Extension はその php へのコンパイル時の処理を変更する目的に用いるようです.
$volt = \Phalcon\Mvc\View\Engine\Volt();
$compiler = $volt->getCompiler();
$compiler->addExtension(new MyExtension());
ドキュメントには下記のメソッドが記載されています.
まあこれでは使い方などさっぱりわからんので例としてそれぞれのメソッドを定義したクラスを示します.
class MyExtension {
public function compileFunction($name, $arguments) {}
public function compileFilter($name, $arguments, $funcArguments) {}
public function resolveExpression($expr) {}
public function compileStatement($statement) {}
}
このように実装したクラスを下記のように追加すればコンパイル時の処理をインターセプトできるという感じのようです.
これらのメソッドのうち resolveExpression() と compileStatement() には基本的に AST のノードで渡されるようです.
AST については volt に限らず一般的な固有名詞なのでググるといろいろ説明が出てくるはずなので説明は割愛します.
例として下記のように if 文の AST ノードを図示しておきます.
{% if a is true %}
Extension の動きを見てみる
さて, Extension はどのような動きをするのでしょうか.実際にコードで見てみます.
コード
下記のようなスクリプトで簡単に出力して確認してみましょう.
ポイントとしては volt のオプションに compileAlways を true に指定しておくことで毎回コンパイルが行われるようにします.
これを付けない場合はエクステンションのメソッドは当然最初のロード一回のみしか呼ばれません.
<?php
class IndexController extends \Phalcon\Mvc\Controller {
public function indexAction() {
}
}
class MyExtension {
public function compileFunction($name, $arguments) {
var_dump(__FUNCTION__);
var_dump($name, $arguments);
}
public function compileFilter($name, $arguments, $funcArguments) {
var_dump(__FUNCTION__);
var_dump($name, $arguments, $funcArguments);
}
public function resolveExpression($expr) {
var_dump(__FUNCTION__);
var_dump($expr);
}
public function compileStatement($statement) {
var_dump(__FUNCTION__);
var_dump($statement);
}
}
$di = new Phalcon\DI\FactoryDefault();
$di->set('volt', function($view, $di) {
$volt = new \Phalcon\Mvc\View\Engine\Volt($view, $di);
$volt->setOptions(array(
'compiledPath' => __DIR__.'/compiled-templates/',
'compiledExtension' => '.compiled',
"compileAlways" => true,
));
$compiler = $volt->getCompiler();
$compiler->addExtension(new MyExtension());
return $volt;
});
$di->set('view', function() {
$view = new \Phalcon\Mvc\View();
$view->setViewsDir(__DIR__.'/views/');
$view->registerEngines(array(
".volt" => 'volt',
));
return $view;
});
$app = new \Phalcon\Mvc\Application($di);
echo $app->handle()->getContent();
例1: html をそのまま出力する場合
まずいちばん簡単なものとして volt の構文が含まれない場合を見てみます.
<!DOCTYPE html>
<html>
</html>
array(4) {
'type' =>
int(357)
'value' =>
string(30) "<!DOCTYPE html>
<html>
</html>"
'file' =>
***
'line' =>
int(3)
}
<!DOCTYPE html>
<html>
</html>
配列が出力されました.またなにやら type に数字が入っている様子です.
そして value には記述されていた html が書かれていますね.
最後に file と line というのは出力に用いたファイルとその中における行数を表しているようです,
357 というのは volt ファイルに直接書かれたものを表しているものだと思います.
ここは特に変更されずそのまま php ファイルに出力されているものと思います.
例2: {{ }}
で出力する場合
次に文字列を出力する場合について見てみます.
{{ "Hello World!" }}
string(16) "compileStatement"
array(4) {
'type' =>
int(359)
'expr' =>
array(4) {
'type' =>
int(260)
'value' =>
string(12) "Hello World!"
'file' =>
***
'line' =>
int(1)
}
'file' =>
***
'line' =>
int(1)
}
string(17) "resolveExpression"
array(4) {
'type' =>
int(260)
'value' =>
string(12) "Hello World!"
'file' =>
***
'line' =>
int(1)
}
<?php echo 'Hello World!'; ?>
file や line などは確実に含まれているようです.
type が 359 のノードは echo に変換されるものです {{ }}
で書かれたものはこれで表されるようです,そして expr があり,expr にあるノードが resolveExpression() に渡されて呼ばれています.
例3: フィルタを使う場合
次にフィルタを使う場合を見てみましょう.
{{ "Hello World!"|e }}
string(16) "compileStatement"
array(4) {
'type' =>
int(359)
'expr' =>
array(5) {
'type' =>
int(124)
'left' =>
array(4) {
'type' =>
int(260)
'value' =>
string(12) "Hello World!"
'file' =>
***
'line' =>
int(1)
}
'right' =>
array(4) {
'type' =>
int(265)
'value' =>
string(1) "e"
'file' =>
***
'line' =>
int(1)
}
'file' =>
***
'line' =>
int(1)
}
'file' =>
***
'line' =>
int(1)
}
string(17) "resolveExpression"
array(5) {
'type' =>
int(124)
'left' =>
array(4) {
'type' =>
int(260)
'value' =>
string(12) "Hello World!"
'file' =>
***
'line' =>
int(1)
}
'right' =>
array(4) {
'type' =>
int(265)
'value' =>
string(1) "e"
'file' =>
*** 'line' =>
int(1)
}
'file' =>
*** 'line' =>
int(1)
}
string(17) "resolveExpression"
array(4) {
'type' =>
int(260)
'value' =>
string(12) "Hello World!"
'file' =>
***
'line' =>
int(1)
}
string(13) "compileFilter"
string(1) "e"
string(14) "'Hello World!'"
NULL
<?php echo $this->escaper->escapeHtml('Hello World!'); ?>
まず compileStatement() で 359 ( echo )というノードになっており,その下に 124 という type のノードがあります.これがフィルタを表すものですね.124 のノードには left と right があり,フィルタで指定した左右の値が含まれているようです.
volt ではこのように一つのノードとして type の値で種類が渡され, value や expr,left や right など必要なものが含まれます.
簡単に流れを表すと下記のような感じになります.
- 最初に compileStatement() が呼ばれる
- その後とにかく resolveExpression() が呼ばれまくる
- type が 350 の場合は compileFunction() が呼ばれる
- type が 124 の場合は compileFilter() が呼ばれる
Statement の一例
statement | type |
---|---|
{{ }} |
359 |
{% autoescape %} |
317 |
html など生の記述 | 300 |
{% if %} |
300 |
expression の一例
expression | type | 備考 |
---|---|---|
フィルタ | 350 | |
文字列 | 260 | php コンパイル時に '' でくくられる |
変数 | 265 | php コンパイル時に $ が先頭につく |
属性 | 46 | aaa.bbb のようにメンバなどにアクセスする場合 |
拡張例
実際に Extension を書いてみましょう.
ここでは 文字列として記述されたものを明示的に指定しなくても全てエスケープ処理を挟むというものを作ってみます.
例えば <b>
のような html タグとなるような文字列が {{ }}
直下にある場合これをそのまま文字列として出力してみます.
{{ '<b>Bold!</b>' }}
volt そのままだと下記のように コンパイルされ, html として出力されます.これを エスケープして表示するようにしてみましょう.
<?php echo '<b>Bold!</b>'; ?>
まず,対象とする Statement は {{ }}
のため type が 359 のものに絞ります
public function compileStatement($statement) {
if ($statement['type'] === 359) {
}
}
次に対象とするのは文字列だけのため, 260 かどうかをチェックします
public function compileStatement($statement) {
if ($statement['type'] === 359) {
$expr = $statement['expr'];
if ($expr['type'] === 260) {
}
}
}
この $expr の value に文字列が入っています.こいつをなんとかすればよさそうなのでこれをエスケープする処理を行います.
ここで compileStatement などはコンパイルされた php ファイルに出力する文字列を返り値に渡すことで変更された内容を反映させることが出来ます.
そのため,直接 php コードを文字列として返すことや AST ノードを構築したものを返すようにする処理が必要になります.
直接 php スクリプトの文字列を返す
とりあえず下記の様にすると目的は達成できます.
public function compileStatement($statement) {
if ($statement['type'] === 359) {
$expr = $statement['expr'];
if ($expr['type'] === 260) {
return '<?php echo $this->escaper->escapeHtml(\''.$expr['value'].'\'); ?>';
}
}
}
AST ノードを構築して php コードを生成する
AST ノードを構築し構築して php コードを生成してみます.
そのために volt のコンパイラを取得します.ここでは Component クラスを継承して $this->di
で DI を取得できるようにしてみています.
class MyExtension extends \Phalcon\Mvc\User\Component {
public function compileStatement($statement) {
if ($statement['type'] === 359) {
$expr = $statement['expr'];
if ($expr['type'] === 260) {
$volt = $this->di->get('volt');
$compiler = $volt->getCompiler();
}
}
}
}
ようするにこういうことを自動でやりたいという話なのでフィルタを使ったノードを生成するようにしてみます.
{{ '<b>Bold!</b>'|e }}
ということで出来たのがこんな感じです.
return で echo php コードをそのまま書いてるのは(このコードではないですが)パターンによっては compileStatement() が無限に再帰的に呼ばれる可能性があるため念のため回避してる感じです.
構築時には file や line は必須ではないためここでは追加してませんが,必要に応じて追加しておいたほうがいいかもしれないです.
public function compileStatement($statement) {
if ($statement['type'] === 359) {
$expr = $statement['expr'];
if ($expr['type'] === 260) {
$volt = $this->di->get('volt');
$compiler = $volt->getCompiler();
$filter = [
'type' => 124,
'left' => $expr,
'right' => [
'type' => 265,
'value' => 'e',
],
];
return '<?php echo '.$compiler->expression($filter).'; ?>';
}
}
}
<?php echo $this->escaper->escapeHtml('<b>Bold!</b>'); ?>
とまあこんな感じで Extension を書くことが出来ます.
問題点など
例えば Extension の実装で for や if のような文法を追加することはできません.
これらは AST ノードを生成する前に解釈されているので Extension の段階ではどうしようもないです.
またノードのタイプについてはドキュメントには全く記載されていないのでコードをよくよむ必要があります.
読むにしてもハードコーディングされていたりいろいろな C のファイルに飛んでいるのでなにがどれに対応するか,どういう動作をするかなどがわかりにくいです.
まとめ
ここでは Volt における Extension について調べてみたことを書いてみました.
まあかなり扱いにくい部類に入るとは思います.使い道としてはドキュメントの例のようにすでにあるリソースを volt で利用するためや,volt で書かれた部分を変更することなく全体的に動作を変更するなどがあるように見えますがまあ黒魔術感が満載ですね.
Volt の Extension についてなにか情報が必要な場合の助けになれば幸いです.