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クラス

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

0 件のコメント:

コメントを投稿

どうかお気軽にコメント頂ければ幸いです。