2010年7月20日火曜日

Twigでテンプレート側からAppを起動

現在、当方では、PHPのテンプレートクラスであるTwigの

可能性を追求しているところです。



今回は、テーマファイルからAppを呼び出す仕組みを開発してみました。



通常のWEBアプリでは、まずAppを呼び出して処理した後に

Appでの実行結果をテーマに書き出すような処理順序になると思います。



今回、この処理順序をちょっと変えて、

テーマファイルに「app=blog fct=EntryView id=3」のように書いておけば

該当箇所にAppの実行結果が自動的に表示されるようにしてみました。

※イメージ的には、SoyCMSでやってるようなことです。
 SoyCMSでは独自テンプレートエンジンでやってると思いますが、
 当方では今回これをTwigという有名なテンプレートエンジン上で
 やってみている、というワケです。



以下、実装クラスのソースコードを晒しておきますので、

ご参考いただければ幸いです。



Extensionクラス

当該クラスで、TokenParserを定義します。(c.f. getTokenParsers())
<?php
/**
* Twig_Extension_Sample
* @author spark43
*
*/
class Twig_Extension_Sample extends Twig_Extension
{
    /**
     * (non-PHPdoc)
     * @see v0.02/Sample/_opt/Twig/Twig_Extension::getTokenParsers()
     */
    public function getTokenParsers()
    {
        return array(
            new Twig_TokenParser_Sample(),
        );
    }


    /**
     * (non-PHPdoc)
     * @see v0.01/Sample/_opt/Twig/Twig_Extension::getFilters()
     */
    public function getFilters()
    {
        return array(
            'nl2br' => new Twig_Filter_Method($this, 'nl2br'),
        );
    }

    /**
     * (non-PHPdoc)
     * @see v0.01/Sample/_opt/Twig/Twig_ExtensionInterface::getName()
     */
    public function getName()
    {
        return 'aither';
    }

    /**
     * nl2br
     * @param $string
     */
    public function nl2br($string)
    {
        return str_replace("\n", "<br />", Twig_Filter_Method);
    }
}

?>




TokenParserクラス

当該クラスでテーマに書かれた構文をトークンに解析します。
使用するフレームワークの構文に合わせることが必要です。
※オープンソースCMSなどに見られる場合があるのですが、
 使用モジュールごとにそれぞれ独自なフレームワークを使っている場合、
 該当フレームワークごとに必要となる場合もあるかと思います。
<?php
/**
* Twig_TokenParser_Sample
* @author spark43
*
*/
class Twig_TokenParser_Sample extends Twig_TokenParser
{
    /**
     * (non-PHPdoc)
     * @see v0.02/Sample/_opt/Twig/Twig_TokenParserInterface::parse()
     */
    public function parse(Twig_Token $token)
    {
        $lineno = $token->getLine();
        $stream = $this->parser->getStream();
        $exp = $this->parser->getExpressionParser();


        // parse
        $request = array();
        $param = array();
        while($stream->test(Twig_Token::NAME_TYPE)) {
            $expk = $exp->parseExpression();
            $stream->expect(Twig_Token::OPERATOR_TYPE, '=');
            $expv = $exp->parseExpression();

            switch ($expk['name']) {
                case 'app':
                case 'fct':
                case 'with':
                    if (! ($expv instanceof Twig_Node_Expression_Name)) {
                        throw new Twig_SyntaxError("'". $expk['name']. "' paameter is invalid", $lineno);
                    }
                    $request[$expk['name']] = $expv;
                    break;
                default:
                    // TODO : other expressions...?
                    if (! ($expv instanceof Twig_Node_Expression_Name)
                    && ! ($expv instanceof Twig_Node_Expression_Constant)) {
                        throw new Twig_SyntaxError("'". $expk['name']. "' paameter is invalid", $lineno);
                    }
                    $param[$expk['name']] = $expv;
                    break;
            }
        }
        $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);


        // check
        if (! isset($request['app'])) {
            throw new Twig_SyntaxError("no 'app' paameter", $lineno);
        }
        if (! isset($request['fct'])) {
            throw new Twig_SyntaxError("no 'fct' paameter", $lineno);
        }
        if (! isset($request['with'])) {
            throw new Twig_SyntaxError("no 'with' paameter", $lineno);
        }


        // return
        return new Twig_Node_Sample(new Twig_Node($param), new Twig_Node($request), $lineno, $this->getTag());
    }


    /**
     * (non-PHPdoc)
     * @see v0.02/Sample/_opt/Twig/Twig_TokenParserInterface::getTag()
     */
    public function getTag()
    {
        // using tag name
        return 'aither';
    }
}
?>





Nodeクラス

当該クラスでトークンをコンパイルします。
TokenParserクラスと同様に、
使用するフレームワークとの整合性に
十分に配慮する必要があります。

<?php
/**
* Twig_Node_Sample
* @author spark43
*
*/
class Twig_Node_Sample extends Twig_Node
{
    /**
     * __construct
     * @param $name
     * @param $value
     * @param $lineno
     * @param $tag
     */
    public function __construct($param, $request, $lineno, $tag = null)
    {
        parent::__construct(array('param' => $param), array('request' => $request), $lineno, $tag);
    }

    /**
     * (non-PHPdoc)
     * @see v0.02/Sample/_opt/Twig/Twig_Node::compile()
     */
    public function compile($compiler)
    {
        $compiler->addDebugInfo($this);


        // prepare request (from token parser)
        $app = $this['request']->app['name'];
        $fct = $this['request']->fct['name'];
        $with = (isset($this['request']->with['name'])
                    ? $this['request']->with['name']
                    : VApp::BY_TPL);
        switch ($with) {
            case VApp::BY_OBJ:
            case VApp::BY_TPL:
                $withX = $with;
                break;
            default:
                $withX = VApp::BY_OBJ;
                break;
        }
        $compiler->write("\$req = new VRequest();\n")
                ->write("\$req->app = '". $app. "';\n")
                ->write("\$req->fct = '". $fct. "';\n")
                ->write("\$req->with = '". $withX. "';\n");

        // prepare parameters
        $param = $this->param;
        foreach ($param as $pk => $pv) {
            if ($pv instanceof Twig_Node_Expression_Name) {
                $compiler->write("\$req->". $pk. "=". $pv['name']. ";\n");
            }
            if ($pv instanceof Twig_Node_Expression_Constant) {
                $compiler->write("\$req->". $pk. "=". $pv['value']. ";\n");
            }
        }

        // execute requested app
        $compiler->write("\$vservice = new ThemeService(\$req);\n")
                ->write("\$aether_result = \$vservice->service();\n");
        switch ($withX) {
            case VApp::BY_OBJ:
                $compiler->write("\$context['". $with. "'] = \$aether_result;\n");
                break;
            case VApp::BY_TPL:
                $compiler->write("echo \$aether_result;\n");
                break;
        }
    }

}
?>




Twigでタグを作成する場合、それも特にNodeクラスを作成する場合には、

キャッシュしておくとcompile結果がキャッシュ・ファイルに出力されるので

compile結果がPHPとして正しく動作するか、

組み込むフレームワークとの連動が正しく行われるか、

実行結果がただしく画面表示されるか、

というようにソースコードのレベルでチェックできると思います。



このように、独自でタグを作成する場合には、

詳述しませんが、

 ・Tokenクラス

 ・TokenStreamクラス

 ・Nodeクラス

 ・Node/Expression/以下の、NodeExpressionクラス

などのソースを熟読することをお勧めしておきます。

2010年7月13日火曜日

twigのクラス階層図

doxygen+graphvizで、twigのクラス階層図を作成してみました。






ダウンロードして、拡大して、参照してみてください。

一応、ご参考まで。(あまり参考になりませんけど。)

Twigでタグを自作

Twigで新しいタグを定義してみました。

タグを自作して、それをTwigに組み込む方法が
少し分かりました。


が、想像以上に面倒ですし、
今でも未だ分からないことがたくさんです。。。



0.概要

(1)作りたいタグ
今回、Twig0.9.8で、次のようなタグを作ってみます。
    {% sample name as 'my tag' %}

一応、ドキュメントでは当該タグの作り方が記述されてますが、
どうやら内部仕様が変わってしまったようでして、
ドキュメント通りにコーディングしても、うまく動作しません。。。

そこで、実際どうやるの?っていうのを調べてみます。


(2)作成手順
以下を見てもらえれば分かるんですが、
大まかに言うと、タグを新しく作るには、
 ■TokenParser:構文解析し、Nodeを生成する
 ■Node:コンパイル時に出力する内容を生成
の2点を新しく作成する必要があるようです。

そして、それらは、
 ■Extension
   ↓
 ■TokenParser
   ・
   ・(コンパイラによるコンパイル)
   ・
 ■Node
のような手順で実行されるようです。



1.Extension定義

filterやtagを登録するのは、基本的に、
Twig_Extensionを継承したクラスから行います。
ここでは、自作したTokenParserを登録します。
<?php
class Twig_Extension_Sample extends Twig_Extension
{
    // !! return a handmaid token-parser !!
    public function getTokenParsers()
    {
        return array(
            new Twig_TokenParser_Sample(),
        );

    }

    public function getName()
    {
        return 'sample';
    }
}

?>




2.TokenParser作成

TokenParserを作成するとき、一応、parserの機能を使ってparseできます。
しかし、具体的に、どんな機能があるかは、
Twig_Parserクラスのソースを読むしかないようです。。。
<?php
class Twig_TokenParser_Sample extends Twig_TokenParser
{
    public function parse(Twig_Token $token)
    {
        $lineno = $token->getLine();

        $stream = $this->parser->getStream();
        $exp = $this->parser->getExpressionParser();

        // !! analyze token !!
        $name = $exp->parseAssignmentExpression();
        $stream->expect(Twig_Token::NAME_TYPE, 'as');
        list(, $value) = $exp->parseMultitargetExpression();
        $stream->expect(Twig_Token::BLOCK_END_TYPE);

        return new Twig_Node_Sample($name, $value, $lineno, $this->getTag());
    }

    // !! this is tag name (in this case, 'sample' tag) !!
    public function getTag()
    {
        return 'sample';
    }

}
?>




3.Node作成

ここでの目玉は、何といってもcompile()ですが、
ここもTwig_Compilerクラスのソースを読むしかないようです。。。
<?php
class Twig_Node_Sample extends Twig_Node
{
    protected $_name = null;
    protected $_value = null;


    public function __construct($name, $value, $lineno, $tag = null)
    {
        parent::__construct(array('name' => $name, 'value' => $value), array(), $lineno, $tag);

        $this->_name = $name;
        $this->_value = $value;
    }

    public function compile($compiler)
    {
        $compiler->subcompile($this->_name, false);
        $compiler->raw(' = ');
        $compiler->subcompile($this->_value);
        $compiler->raw(";\n");
    }

}
?>




これらにより、実際のコンパイル結果には、
次のように出力されます。
$context['name'] = "viva";

たったこれだけ出力するだけで、結構な労力が必要ですね。。。

タグを自作する場合には、どうしても必要な場合だけに
限ったほうがいいと思います

でも、どうしても必要な場合は、果たしてあるんでしょうか・・・?
それよりも、Twigを使う場合のViewクラスなどを工夫したほうが
よさそうな気がしますが、どうでしょう?

2010年7月7日水曜日

Twigで自作フィルタ

Twigでは色々なものを自作して組み込むことができるそうで、
フィルタやタグは勿論のこと、何だったらparserとかも自作して
組み込むことができそうです。


今回、Twigのフィルタを自作して組み込んでみたので、
その方法について、軽く整理しておきます。


今回作成するのは、nl2br(改行コードをbrタグに変換)です。
信じられませんが、意外とこれが無かったりします。


1.フィルタ本体



class Sample_Extension extends Twig_Extension
{
    public function getFilters()
    {
        return array('nl2br' => new Twig_Filter_Method($this, 'nl2br'));
    }

    public function nl2br($value, $sep = '<br />')
    {
        return str_replace("\n", $sep."\n", $value);
    }

    public function getName()
    {
        return 'sample';
    }
}

ポイントは2点。
 ■Twig_Extensionを継承して作り、getName()を実装
 ■フィルタとして登録する機能をgetFileters()で返す
  (このとき、PHP関数なのかクラスメソッドなのか
   インスタンスメソッドなのかで実装方法が異なりますので、
   詳しくはドキュメントを読みましょう。)

※ここでは、スゲー適当なクラス名で作成し、
 スゲー適当なファイル名で保存しました。

※Twig標準のautoloaderに読み込ませる場合には、
 真面目に命名し、真面目なファイル名で保存し、
 真面目なフォルダに保存しておきましょう。
 Twig/Extension/Core.phpや
 Twig/Autoloader.phpなど
 のソースを読めば、すぐに理解できると思います。

 

2.Twigへの組み込み



$twig->addExtension(new Sample_Extension());

上記1で作成したフィルタを、Twig_EnviromentにaddExtensionで追加します。



・・・以上です。

簡単でしたね。

合格証受領

合格証が届きました。

先週土曜日、所用で東京に行っていたのですが、

そのときに配送されていたようです。

第18回ITコーディネータ試験に合格しました。



8月下旬頃から研修が始まりますので、

その申し込みと、開始までにより一層のお勉強と、

・・というのを片手間にやりつつの、本業と。

忙しい日々が続きますが、頑張ります。