PowerCMS X ブログ

2024-12-14

DomDocumentを使って MTMLをパースする(PAMLクラスの処理を追う)

PAMLクラスは 1ファイル、非常に軽量で高速に動作するテンプレートエンジンです。PowerCMS Xのビュー(テンプレート)をビルドするものという理解をされているかと思うのですが、実はヘッドレスCMSから JSONを取得して MTMLで組み立てるようなこともできなくはありません。

どのようにMTMLをパースして、PHPコードに変換しているかについて、処理がよく理解できない、という社内の声がありまして、いつか纏めなきゃね、ということで本日の記事となります。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title><mt:var name="title" escape></title>
  </head>
  <body>
    <main>
    <h1><mt:var name="title" escape></h1>
    <h2>テンプレートのビルドテスト</h2>
    <p id="headline"><mt:var name="description" escape></p>
    <mt:loop name="list_items">
      <mt:if name="__first__"><ul></mt:if>
        <li><mt:var name="__value__"></li>
      <mt:if name="__last__"></ul></mt:if>
    </mt:loop>
    </main>
  </body>
</html>

このようなテンプレートがあった時、以下のコードで処理した結果を出力できます。

$mtml = file_get_contents( 'template.tmpl' );
$ctx = new PAML();
$ctx->init();
$ctx->prefix = 'mt';
$ctx->vars['title'] = 'タイトル';
$ctx->vars['description'] = '記事の概要です。';
$ctx->vars['list_items'] = [1,2,3];
echo $ctx->build( $mtml );

compile メソッドの処理を追う

この時、内部的には compileメソッドを通っていて、このメソッドがテンプレートエンジンの心臓部となります。

compile( string $content, bool $disp = true, array|null $tags_arr = null, array $use_tags = [],
         array $params = [], bool $compiled = false, bool $nocache = false, string|null $orig_key = null ) : string

第6引数の $compiled に trueを渡すと、コンパイル済みの PHPコードを返します。

$compiled = $ctx->compile( $html, true, null, [], [], true );
echo '<pre>';
echo htmlspecialchars( $compiled );

処理の途中のテンプレートの状態とともに、やっていることを簡単に解説します。

  • HTMLの開始タグと終了タグをダミー文字列(コンテンツに含まれないランダムな文字列)に置き換える
  • MTタグは {%mtvar name="title" escape /%} の形式に置き換える
%_2795dd!DOCTYPE html_2795dd%
%_2795ddhtml lang="ja"_2795dd%
  %_2795ddhead_2795dd%
    %_2795ddmeta charset="utf-8"_2795dd%
    %_2795ddtitle_2795dd%{%mtvar name="title" escape /%}%_2795dd/title_2795dd%
  %_2795dd/head_2795dd%
  %_2795ddbody_2795dd%
    %_2795ddmain_2795dd%
    %_2795ddh1_2795dd%{%mtvar name="title" escape /%}%_2795dd/h1_2795dd%
    %_2795ddh2_2795dd%テンプレートのビルドテスト%_2795dd/h2_2795dd%
    %_2795ddp id="headline"_2795dd%{%mtvar name="description" escape /%}%_2795dd/p_2795dd%
    {%mtloop name="list_items"%}
      {%mtif name="__first__"%}%_2795ddul_2795dd%{%/mtif%}
        %_2795ddli_2795dd%{%mtvar name="__value__" /%}%_2795dd/li_2795dd%
      {%mtif name="__last__"%}%_2795dd/ul_2795dd%{%/mtif%}
    {%/mtloop%}
    %_2795dd/main_2795dd%
  %_2795dd/body_2795dd%
%_2795dd/html_2795dd%
  • MTタグのみ戻して(「:」を削除、タグは小文字に揃える)
  • $ctx->parse_literal(setvartemplate、dynamicmtml、literal)は正規表現で配列に退避する
  • その後、DomDocumentでパースする
  • include、block、conditional、functionの順にループ処理して PHPのネイティブコードに変換していく※
  • PHPコードを textNodeとして組み立て、appendChild、replaceChild、insertBefore(ブロックタグ)でノードを置き換えていき、最後に saveHTMLでPHPコードにします(その後HTMLの開始タグと終了タグを元に戻す)。

パースする直前のテンプレートの状態

<dom__73c019>%_2795dd!DOCTYPE html_2795dd%
%_2795ddhtml lang="ja"_2795dd%
  %_2795ddhead_2795dd%
    %_2795ddmeta charset="utf-8"_2795dd%
    %_2795ddtitle_2795dd%__dom__73c019__<mtvar name="title" escape />__dom__73c019__%_2795dd/title_2795dd%
  %_2795dd/head_2795dd%
  %_2795ddbody_2795dd%
    %_2795ddmain_2795dd%
    %_2795ddh1_2795dd%__dom__73c019__<mtvar name="title" escape />__dom__73c019__%_2795dd/h1_2795dd%
    %_2795ddh2_2795dd%テンプレートのビルドテスト%_2795dd/h2_2795dd%
    %_2795ddp id="headline"_2795dd%__dom__73c019__<mtvar name="description" escape />__dom__73c019__%_2795dd/p_2795dd%
    __dom__73c019__<mtloop name="list_items">__dom__73c019__
      __dom__73c019__<mtif name="__first__">__dom__73c019__%_2795ddul_2795dd%__dom__73c019__</mtif>__dom__73c019__
        %_2795ddli_2795dd%__dom__73c019__<mtvar name="__value__" />__dom__73c019__%_2795dd/li_2795dd%
      __dom__73c019__<mtif name="__last__">__dom__73c019__%_2795dd/ul_2795dd%__dom__73c019__</mtif>__dom__73c019__
    __dom__73c019__</mtloop>__dom__73c019__
    %_2795dd/main_2795dd%
  %_2795dd/body_2795dd%
%_2795dd/html_2795dd%</dom__73c019>
  • ブロックタグは、while文で囲って、クラス->メソッドを呼び出す、さらにグローバルモディファイアでラップする($ctx->add_modifierメソッド)
  • 条件タグは if文で囲って、クラス->メソッドを呼び出す
  • ファンクションタグはクラス->メソッドを呼び出す、さらにグローバルモディファイアでラップする
  • ブロックタグと条件タグ開始時と終了時にローカル変数を初期化する
  • ブロックタグは開始箇所に ob_start、終了箇所の ob_get_clean で結果を取得する
  • タグの第一引数は $ctx->setup_args(タグ属性とタグ名の配列)メソッドの結果で、タグ属性に変数が渡された場合や配列のキーを指定している場合など、適切な形に展開する
  • グローバルモディファイアは paml_htmlspecialchars, paml_rawurlencode, paml_modifier_funcs を除いて クラス->メソッド の形で呼び出す($ctx->add_modifierメソッド)
  • 最後に、HTMLの開始タグと終了タグを元に戻す

最終的なPHPコード

<?php $_vars=&$this->vars;$_old_params=&$this->old_params;$_local_params=&$this->local_params;$_old_vars=&$this->old_vars;$_local_vars=&$this->local_vars;?><!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title><?php echo paml_htmlspecialchars($this->function_var($this->setup_args(['name'=>'title','escape'=>'','this_tag'=>'var'],null,$this),$this),ENT_QUOTES)?>
</title>
  </head>
  <body>
    <main>
    <h1><?php echo paml_htmlspecialchars($this->function_var($this->setup_args(['name'=>'title','escape'=>'','this_tag'=>'var'],null,$this),$this),ENT_QUOTES)?>
</h1>
    <h2>テンプレートのビルドテスト</h2>
    <p id="headline"><?php echo paml_htmlspecialchars($this->function_var($this->setup_args(['name'=>'description','escape'=>'','this_tag'=>'var'],null,$this),$this),ENT_QUOTES)?>
</p>
    <?php $c_6f6ea6=null;$_old_params['_6f6ea6']=$_local_params;$_old_vars['_6f6ea6']=$_local_vars;$a_6f6ea6=$this->setup_args(['name'=>'list_items','this_tag'=>'loop'],null,$this);$_6f6ea6=-1;$r_6f6ea6=true;while($r_6f6ea6===true):$r_6f6ea6=($_6f6ea6!==-1)?false:true;echo $this->block_loop($a_6f6ea6,$c_6f6ea6,$this,$r_6f6ea6,++$_6f6ea6,'_6f6ea6');ob_start();?>
<?php $c_6f6ea6 = true; if(isset($this->local_vars['__total__'])&&isset($this->local_vars['__counter__'])&&$this->local_vars['__total__']<$this->local_vars['__counter__']){$c_6f6ea6=false;}if($c_6f6ea6 ):?>
      <?php $_old_params['_fe4142']=$_local_params;$_old_vars['_fe4142']=$_local_vars;if($this->conditional_if($this->setup_args(['name'=>'__first__','this_tag'=>'if'],null,$this),null,$this,true,true)):?>
<ul><?php endif;$_local_params=$_old_params['_fe4142'];$_local_vars=$_old_vars['_fe4142'];?>
        <li><?php echo $this->function_var($this->setup_args(['name'=>'__value__','this_tag'=>'var'],null,$this),$this)?>
</li>
      <?php $_old_params['_544a4d']=$_local_params;$_old_vars['_544a4d']=$_local_vars;if($this->conditional_if($this->setup_args(['name'=>'__last__','this_tag'=>'if'],null,$this),null,$this,true,true)):?>
</ul><?php endif;$_local_params=$_old_params['_544a4d'];$_local_vars=$_old_vars['_544a4d'];?>
    <?php endif;$c_6f6ea6=ob_get_clean();endwhile; $_local_params=$_old_params['_6f6ea6'];$_local_vars=$_old_vars['_6f6ea6'];?>
    </main>
  </body>
</html>

結果は compile_dir 配下に md5(テンプレートのソース).php ファイル名で保存して、それがあれば次回はそれを使います。

PowerCMS Xのビューや URLマップは、保存時に compiledカラムに結果を保存し、それを使います。

いかがでしたでしょうか。せっかくなので、プラグインなどから利用できるPAMLのいくつかのメソッドについても紹介します。

PAMLクラス

PAML : PHP Alternative Markup Language

set_loop_vars( int $counter, array $params ) : null

予約変数「__first__」「__last__」「__even__」「__odd__」「__index__」「__counter__」「__end__」「__total__」をローカル変数にセットする

$ctx->set_loop_vars( $counter, $params );

get_any ( string $name, string $var = null, bool $recursive = false ) : mixed

ローカル変数、グローバル変数いずれかににマッチする値を返す $var には「local_vars」「vars」が指定可能、省略時はローカル変数、グローバル変数の順に検索する

$ctx->get_any( $name );

get_template_path ( string $path, bool $continue = true ) : string|null

プロパティ「template_paths(ディレクトリのパスの配列)」から該当の(優先)テンプレートファイルのパスを返す

$ctx->get_template_path( 'edit.tmpl );

append_prepend ( string $str, array $args, string $name, mixed $var = null ) : string

append, prepend, functionタグ属性に沿って処理を行う

$ctx->append_prepend( $content, $args, $name, $var );

localize( array $names ) : null

変数をブロック内に局所化する(local_varsについては局所化の必要はありません)

$ctx->localize( ['var1', 'var2'...] );

restore( array $names ) : null

局所化した変数を元に戻す

$ctx->restore( ['var1', 'var2'...] );

conditional_if( array $args, string $content, class $ctx, bool $repeat, bool $context ) : string

条件タグで eq などの比較が可能になる

$ctx->conditional_if( $args, $content, $ctx, $repeat, $context );

static math_operation( string $op, int|string $value1, int|string $value2, array $args = [] ) : int

PAML::math_operation( $args['op'], $value, $content, $args );

opモディファイアの処理を行う

false() : bool

ブロックタグで第4引数 $repeat に値をセットすることでループを終了する

$repeat = $ctx->false();

ブロックタグのシンプルなサンプルコード

    function hdlr_blocktag_name ( $args, $content, $ctx, &$repeat, $counter ) {
        // $app = $ctx->app;
        $local_vars = ['local_var1', 'local_var2', 'local_var3'];
        if (! $counter ) {
            $loop_params = ['var1', 'var2', 'var3'];
            if ( empty( $loop_params ) ) {
                $repeat = $ctx->false(); // ループをスキップする時
                return;
            }
            $ctx->localize( $local_vars ); // 変数のローカライズ
            $ctx->local_params = $loop_params;
        }
        $params = $ctx->local_params;
        $ctx->set_loop_vars( $counter, $params ); // 予約変数をセット
        if ( isset( $params[ $counter ] ) ) {
            $param = $params[ $counter ];
            $ctx->local_vars['__value__'] = $param; // ローカル変数 __value__ に値をセット
            $repeat = true;
        } else {
            $ctx->restore( $local_vars ); // 変数のリストア
            $repeat = $ctx->false(); // ループを終了
        }
        return $content;
    }

カテゴリー:技術情報

投稿者:Junnama Noda

ブログ内検索

アーカイブ


日本語
ふりがな付き
English
简体中文
繁體中文
한국어