General

skin.php

2007年12月24日

スキンのパース部分がほぼ出来上がったので、メモ。

スキンの仕様としては、Nucleusのスキンのアッパーコンパチブルにすることをめざす。すなわち、<%skinvar%>の様な記述で書き上げるやり方をとる。

一方で、<?php … ?>のような、PHP直書きの表記も使えるようにする。したがって、スキンに特別な機能を入れたい場合に、PHPでガリガリ書くことも可能。

もう一つの特徴は…。これはもし完成した場合にJeans CMS の大きな特徴になると思うのだけれど…。スキンをコンパイルして、PHPファイルに変換できるようにしたい。つまり、コンパイルの結果、コアやプラグインの機能を直接呼び出すようなindex.phpが作成される。これをサーバーにおけば、ZAPAさんの改造のように、100倍とまでは行かないものの、かなりのスピードアップが期待できると思われる。

では、順に:
<?php
/* begin xml
<?xml version='1.0'?>
<document>
  <skinvars>
    <class>skin</class>
    <type>normal</type>
    <skinvar><name>testskinvar</name><method>testmethod</method></skinvar>
    <if><name>testskinvar2</name><method>testmethod2</method></if>
  </skinvars>
</document>
/* end xml */

class skin {
    static public $obj;
    static public function init(){
        if (isset(self::$obj)) return;
        self::$obj=new self();
    }
    public $db,$skinvars;
    private function __construct(){
        $this->db=$db=sqlite_open(DIR_SQLITE.'.dbskin');
        sqlite_create_function($this->db,'php','pi');
        // Get type
        if (strpos(realpath('./'),realpath(DIR_JEANS))===0) $type='admin';
        else $type='normal';
        // Check authority to take the action.
        if ($type=='admin' && !member::isAdmin()) {
        
        }
        // Get skinvar table data.
        $query=sql::fill('SELECT subtype, name, method FROM skinvar WHERE type=<%1%>',$type);
        $res=sqlite_unbuffered_query($db,$query);
        $data=array();
        while($row=sqlite_fetch_array($res,SQLITE_ASSOC)){
            $data[$row['subtype']][$row['name']]=$row['method'];
        }
        $this->skinvars=$data;
    }
    public function __destruct(){
        sqlite_close($this->db);
    }
    public function query($query){
        return sqlite_query(self::$obj->db,$query);
    }

 最初に気が付くのは、XMLデータがコメントとして埋め込まれていること。ここに、それぞれのクラスがどんなスキン変数を持っているのかを記述しておく(アクションやイベントも、同様に記述する)。管理画面では、このデータをもとに、スキン変数管理のためのSQLテーブルを作成することが出来るので、そのデータをもとにスキンパースを行う。__construct() で、SQLテーブルからデータを取り出して、$skinvarsというプロパティに代入している。

    static public function selector($parse=true){
        if (!($skinname=@$_REQUEST['skin'])) $skinname=CONF_DEFAULT_SKIN;
        $type='index';
        if ($parse) self::parse($skinname,$type);
        else return array($skinname,$type);
    }
    static public function parse($skinname, $type){
        $compiled=self::compile($skinname,$type);
        eval("?>$compiled");
    }

 次に、セレクタとパース部分。セレクタはまだ書きかけ。パース部分では、次に述べるコンパイルのルーチンで処理されたコードを、eval する。これがNucleusとの違いで、<?php … ?>によるPHP直書きが可能になっている。

    static public function compile($skinname, $type){
        // Check the args;
        if (preg_match('![^a-zA-Z0-9_/\-\.]!',$skinname) || 
            preg_match('![^a-zA-Z0-9_/\-\.]!',$type)) exit('Skin name error');
        // Get the skin file
        if (!file_exists(DIR_SKINS.$skinname.'/'.$type.'.inc')) exit ('Skin file not found');
        $skin=file_get_contents(DIR_SKINS.$skinname.'/'.$type.'.inc');
        // Compile the skin
        $pattern=array('/<%([a-zA-Z0-9_]+)%>/','/<%([a-zA-Z0-9_]+)\((([^\)]|\)[^%]|\)%[^>])+)\)%>/');
        $replace=array('skin','parse_callback');
        return preg_replace_callback($pattern,$replace,$skin);
    }
    static public function parse_callback($matches){
        // Prepare args to be used as a string.
        if (count($matches)==2) {
            $args="('$matches[1]')";
        } else {
            $args="('$matches[1]'";
            foreach(preg_split('/,/',$matches[2]) as $arg){
                $arg=rawurldecode($arg);
                $arg=addslashes($arg);
                $args.=",'$arg'";
            }
            $args.=")";
        }
        // Compile the skin var.
        // The if skinvars and related ones are directly defined in this method.
        // Everything else is defined in skinvar table.
        switch($matches[1]){
        case 'if':
            return "<?php if (skin::parse_if $args == true) { ?>";
        case 'ifnot':
            return "<?php if (skin::parse_if $args == false) { ?>";
        case 'else':
            return "<?php } else { ?>";
        case 'elseif':
            return "<?php } elseif (skin::parse_if $args == true) { ?>";
        case 'elseifnot':
            return "<?php } elseif (skin::parse_if $args == false) { ?>";
        case 'endif':
            if (count($matches)==2) return "<?php } ?>";
            else return '<?php '.str_repeat('}',$matches[2]).'?>';
        default:
            $method=self::$obj->skinvars['skinvar'][$matches[1]];
            return "<?php $method $args; ?>";
        }
    }

 ここが、このツールの特徴の一つ。ここで、正規表現を使って<% … %>記法をPHPコードに置き換える操作を行っている。Nucleusでの経験で、条件分岐としてはif文だけでよいことが分かるから、if関連はここで直接処理している。それ以外のスキン変数については、return "<?php $method $args; ?>";のところで、メソッド呼び出しのPHPコードに変換している。

    static public function parse_if(){
        // Method that define the condition can be found in skinvar table.
        $args=func_get_args();
        $ifskinvar=array_shift($args);
        $skinname=array_shift($args);
        $method=self::$obj->skinvars['if'][$skinname];
        return call_user_func_array(preg_split('/::/',$method),$args);
    }
    static public function testmethod(){
        core::putsbr('skin::testmethod() is called as testskinvar!');
    }
    static public function testmethod2(){
        return (@$_GET['answer']=='yes');
    }
}

最後に、parse_if の部分。ここから外部メソッドを呼び出し、その結果をbool値として返すようになっている。最後の2つのメソッドは、テスト用に作成したもので、最初のXMLデータ部分に対応したものである。

コメント

コメントはありません

コメント送信