開発畑トップ   »  PHP   »  今日からプログラマー!PHP入門【テンプレートエンジン作成編】   »  今日からプログラマー!PHP入門(第12回)

今日からプログラマー!PHP入門(第12回)

もう6月です、早いですね。
今年は色々なことにチャレンジしているのですが、チャレンジの1つとして6月23日にWebセミナーのスピーカーをさせていただけることになりました♪

関西エンジニアが語る「Webデザイナ・HTMLコーダに知っておいて欲しい開発知識・開発ツール」 : ATND http://atnd.org/events/29752 @atndさんから
で、テンプレートエンジンについて話します。(うーん、タイムリーw)
あ、テンプレートエンジンって言っても、うちのオレオレテンプレートじゃなくて、smartyというメジャーなテンプレートエンジンについてです。
よかったら聴きに来て下さいませ~【宣伝】

さて、宣伝も終わったので今回もはじめますよ、今プロPHP第12回です!


前回はパースエラーを事前にチェックするプログラムを作ってみました。
今回はそのプログラムをテンプレートエンジンに組み込んで、さらにちょっとテンプレートエンジンを進化させてみましょう。

テンプレートエンジンの中身

で、できあがったコードがこちらです。
あと、一式入った圧縮ファイルはこちらです。
※圧縮ファイルは、PHPの動く環境(PHP5.2以上お勧め)にアップロードしてindex.phpをブラウザで見れば動きます。

では、template.class.phpのコードを見ていきましょう。長いので覚悟してくださいw

<?php
/*
 * template.class.php v0.1
 * http://rone.jp
 * 
 * Copyright 2012, Shoichi Takahashi
 * Dual licensed under the MIT or GPL Version 2 licenses.
 * 
 * Date: 2012/06/05
 */

class template {
	protected $template;
	protected $resource;
	protected $assignVal;
	protected $safePhpCode;
	/* ------------------------------------------------------------------------
	 * template class constructor
	 */
	public function __construct() {
		/*
		 * original template engine
		 */
		$this->template			= '';
		$this->resource			= '';
		$this->assignVal		= array();
		$this->compileMode		= false;
		/*
		 * set functions
		 * this is check for eval.
		 */
		$funcs = get_defined_functions();
		$this->funcs = array_merge($funcs['internal'],$funcs['user']);
		unset ($funcs);
		$safePhpCode = array('isset', 'count', 'is_array', 'is_file', 'is_dir', 'time', 'empty');
		foreach ($safePhpCode as $safeCode) {
			unset($this->funcs[array_search($safeCode, $this->funcs)]);
		}
	}
	/*
	 * template class destructor
	 */
	public function __destruct() {
	}

	public function compileMode($mode) {
		// $mode = 0 : include mode / 1 : eval mode
		$this->compileMode = $mode;
	}

	public function tplAssign($tplVar, $value) {
		$this->assignVal[$tplVar] = $value;
	}

	public function tplSetup($file) {
		if (file_exists($file) == false) return false;
		$this->template = $file;
		$this->resource = file_get_contents($file);
		return true;
	}

	public function tplDisplay() {
		$this->tplFetch(true);
	}

	public function tplFetch($display = false) {
		/*
		 * output error message, if resouce include php code.
		 * php code check & error
		 */
		if (preg_match("/<\?php/", $this->resource)) {
			$error = "can't use php code.";
			exit($error);
		}
		/*
		 * function check for inside 'if'
		 */
		$matches = array();
		preg_match_all("/\{if[ ]+?(.+?)\}/", $this->resource, $matches);
		if (count($matches[1])) {
			foreach ($matches[1] as $code) {
				$code = preg_replace("/[ \t\r\n]/", "", $code);
				foreach ($this->funcs as $func) {
					if(strpos($code,$func.'(') !== false) {
						$error =  "can't use php function '".$func."()' inside 'if tag'.";
						exit($error);
					}
				}
			}
		}
		/*
		 *  'if' replace to php code
		 *  usage: $test is data.
		 *         {if $test > 100} over {else} under {/if}
		 *         {if $test < 0} under 0 {elseif $test < 10} under 10{else} over 10{/if}
		 */
		$dat = preg_replace("/\{if[ ]+?(.+?)\}/", "<?php if (\\1) { ?>", $this->resource);
		$dat = preg_replace("/\{elseif[ ]+?(.+?)\}/", "<?php }elseif (\\1) { ?>", $dat);
		$dat = preg_replace("/\{else\}/", "<?php }else{ ?>", $dat);
		$dat = preg_replace("/\{\/if\}/", "<?php } ?>", $dat);
		/*
		 *  'foreach' replace to php code
		 *  usage: $test is data.
		 *         {foreach $test as $key => $val} key is {$key}, val is {$val}{/foreach}
		 *         {foreach $test as $val} val is {$val}{/foreach}
		 */
		$dat = preg_replace("/\{foreach[ ]+?(.+?)[ ]+?as (.+?)=>(.+?)\}/", "<?php if (is_array(\\1) && count(\\1)) { foreach (\\1 as \\2 => \\3) { ?>", $dat);
		$dat = preg_replace("/\{foreach[ ]+?(.+?)[ ]+?as (.+?)\}/", "<?php if (is_array(\\1) && count(\\1)) { foreach (\\1 as \\2) { ?>", $dat);
		$dat = preg_replace("/\{\/foreach\}/", "<?php }} ?>", $dat);
		/*
		 *  'for' replace to php code
		 *  usage: $count is data.
		 *         {for $i from 0 to $count step $i++} No. {$i}{/for}
		 */
		$dat = preg_replace_callback("/\{for[ ]+?(.+?)[ ]+?from[ ]+?(.+?)to[ ]+?(.+?)[ ]+?step[ ]+?(.+?)\}/",
			create_function(
				'$matches',
				'if ($matches[2] <= $matches[3]) {
					return "<?php for ($matches[1] = $matches[2]; $matches[1] <= $matches[3]; $matches[4]) { ?>";
				 }else{
					return "<?php for ($matches[1] = $matches[2]; $matches[1] >= $matches[3]; $matches[4]) { ?>";
				 }'
				), $dat);
		$dat = preg_replace("/\{\/for\}/", "<?php } ?>", $dat);

		/*
		 * date
		 */
		$dat = preg_replace("/{dateFormat[ ]+?([\"\'].+?[\"\'])}/", "<?php echo date(\\1 \\2); ?>", $dat);
		$dat = preg_replace("/{dateFormat[ ]+?([\"\'].+?[\"\']),(.+?)}/", "<?php echo date(\\1,\\2); ?>", $dat);

		/*
		 * $a='xxx';
		 * $a=1;
		 * $a=$b['x'];
		 * $a++;
		 * $a--;
		 */
		$varParam = '[\$][a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\[\]\'\"\$]*?';
		$dat = preg_replace('/{('.$varParam.'[ ]*?=[ ]*?[\'|"]+?.*?[\'|"]+?)}/'	, "<?php \\1; ?>", $dat);
		$dat = preg_replace('/{('.$varParam.'[ ]*?=[ ]*?[-0-9\.]+?)}/'			, "<?php \\1; ?>", $dat);
		$dat = preg_replace('/{('.$varParam.'[ ]*?=[ ]*?'.$varParam.')}/'		, "<?php \\1; ?>", $dat);
		$dat = preg_replace('/{('.$varParam.'[ ]*?[\x2b][\x2b])}/'				, "<?php \\1; ?>", $dat);
		$dat = preg_replace('/{('.$varParam.'[ ]*?[\x2d][\x2d])}/'				, "<?php \\1; ?>", $dat);
		$dat = preg_replace('/{('.$varParam.'[ ]*?=[ ]*?.+?)}/'					, "<?php \\1; ?>", $dat);

		/*
		 * echo
		 */
		$dat = preg_replace('/\{('.$varParam.')\}/', "<?php echo \\1; ?>", $dat);
		// include resource data
		$this->resource = $this->IncludeText($dat);
		if ($display) {
			echo $this->resource;
		}else{
			return $this->resource;
		}
	}

	private function IncludeText($text){
		/*
		 * variable assign
		 */
		foreach ($this->assignVal as $key=>$val) {
			$$key = $val;
		}

		if($this->CheckParseError($text)) $this->ErrorMessage('Parse error.');

		if ($this->compileMode) {
			ob_start();
			try {
				$return = eval('?>'.$text);
			}catch(Exception $ex) {

			}

			$contents = ob_get_contents();
			if ($return === false && strstr($contents, '<b>Parse error</b>:')) {
				exit($contents);
			}
			ob_end_clean();
		}else{
			ob_start();
		 	$filename = tempnam('/tmp/','bx');
		 	file_put_contents($filename,$text);
		 	include $filename;
		 	$contents = ob_get_contents();
		 	ob_end_clean();
		 	unlink($filename);
		}
 		return $contents;
 	}

	private function CheckParseError($code) {
		$quote = 0;
		$braces = 0;
		foreach (token_get_all($code) as $token) {
			switch ($token) {
				case "'":
				case '"':
					$quote++;
					break;
				case '{':
					$braces++;
					break;
				case '}':
					$braces--;
					if ($braces < 0) break 2;
					break;
			}
		}

		if ($quote % 2 || $braces != 0) {
			return true;
		}else{
			return false;
		}
	}

	private function ErrorMessage($msg) {
		exit($msg);
	}

}

どんどんプログラムが長くなってきますね。
でも、第1回から順を追って追加追加でコードを足しているので、今回覚えることは多くはありません。頑張っていきましょう。
では、コードを見ていきます。

  • 第1行目~10行目
    phpの開始タグとコメントです。
    今回から、ライセンス表記をしました。といっても「自由に使ってね」というのを明文化しただけですのでご安心を(^-^)
    オープンソースなプログラムではこのようなライセンス表記をすることが大事です。
    ※ライセンス表記が無いと、他の人が使っていいのか解らないですからね。
  • 11行目~16行目
    以前と変更点無しです。
  • 17行目~38行目
    コンストラクタメソッドでは、1点だけ(26行目の「$this->compileMode = false」の一文)を追加しました。
    この変数は、evalモードとincludeモードを切り替えます。詳しい内容は44行目で説明します。
  • 39行目~43行目
    以前と変更点無しです。
  • 44行目~47行目
    新しく追加したメソッドです。
    このメソッドは先述の「$this->compileMode = false」の値を変化させるのに使います。
    compileModeメソッドで「0」をセットするとtplFetchメソッドで処理する方法がincludeに、「1」をセットするとevalで処理するようになります。
    ※いままでこの処理を入れるの忘れてたのでincludeモードでしか動かなかったのです^^;
  • 48行目~59行目
    各メソッドとも変更点無しです。
  • 60行目~108行目
    以前と変更点無しです。
  • 109行目~118行目
    for文のテンプレートタグですが、以前の内容から変更があります。
    以前のタグだと、for文で「0から10に1つづつ増える」という処理はできたのですが、「10から0に1つづつ減る」という処理が出来ませんでした。
    なので、今回新しく覚えるpreg_replace_callback()関数と、create_function()関数を使って作り直してみました。
    preg_replace_callback()関数は、以前覚えたpreg_replace()関数をさらに便利にした関数です。 preg_replace()関数では、正規表現で取得した値を変更するだけでしたが、preg_replace_callback()関数では、取得した値を加工して(関数に値を渡して)から変更することが出来ます。
    たとえば今回のように、正規表現で取得した値を比較して処理を変える場合などはpreg_replace()関数では対応できないので、とても便利です。
    そして、もうひとつの関数create_function()は、匿名関数を作成します。
    簡単に言うと、その場限りの関数を作っちゃうことが出来ます。
    第1引数には、正規表現でマッチした値(配列)が入ります。
    第2引数には、関数の中身(処理内容)を入力します。
    ここでは以下のコードを入れています。
    if ($matches[2] <= $matches[3]) {
      return "<?php for ($matches[1] = $matches[2]; $matches[1] <= $matches[3]; $matches[4]) { ?>";
    }else{
      return "<?php for ($matches[1] = $matches[2]; $matches[1] >= $matches[3]; $matches[4]) { ?>";
    }
    この処理で、正規表現で取得した値(for文中身)が増えるのか減るのかを判別して処理を切り替えています。
  • 119行目~123行目
    新しくタグ「dateFormat」を作成しました。
    このタグは、{dateFormat "Y:m:d H:i:s"}とテンプレートファイルに書くと現在の日付(及び時間)を表示します。
    また、Unixタイムスタンプ(1970年1月1日からカウントされている数値)を渡すと、その日付を表示します。
    処理自体はPHPのdate()関数をそのまま使っていますので、詳しい内容はリンク先を参照ください。
  • 124行目~149行目
    以前と変更点無しです。
  • 150行目~179行目
    このメソッドでは、157行目の追加と、158行目の変更を行いました。
    157行目では、前回のパースエラーチェックの処理を追加しています。
    $this->CheckParseError()メソッド(後述)でパースエラーがあるかどうかをチェックして、問題があれば$this->ErrorMessage()メソッドでエラーを発生させて終了するようにしています。
    158行目では、コンパイルモードをチェックして処理を切り替えています。
    前回のコードと似ていますが、今回はちゃんとcompileModeメソッドで処理を切り変えれるようにしています。
    それ以外の部分は変更なしです。
  • 180行目~203行目
    前回作成したパースエラーチェックの中身です。
    処理自体は前回作成したものと変わりなしです。
  • 204行目~206行目
    エラーメッセージの処理用のメソッドです。
    特に変わった処理はしていないので、割愛しますw

以上がtemplate.class.phpの中身になります。
実際の使い方はアップした圧縮ファイルの/data/template.htmlをみればだいたいわかると思います。
というわけで、今回はこれで終了です。
次回はキャッシュについてです。

今回覚えた関数

コメントをどうぞ!