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

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

今週も何とか更新w(うっかり忘れてたわけですが…)

では始めましょう、今日からプログラマー!PHP入門第14回です。

さて、今回で一旦テンプレートエンジン編を終了する予定です。
次回からの予定はまだ未定なのですが、もうちょっと簡単なものにしようかなと思っています。


前回はテンプレートエンジンのキャッシュを実装する方法を考えました。
今回は実際に実装したコードを見ていきます。

テンプレートエンジンのコードを解説

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

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;
		$this->cache			= false;
		$this->cacheDir			= './cache';
		$this->cacheLoad		= 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 cacheMode($mode) {
		$this->cache = $mode;
	}

	public function cacheDir($dir) {
		$this->cacheDir = $dir;
	}

	public function cacheClear() {
		$dir = $this->cacheDir;
		if($dh = opendir($dir)){
			while(($file = readdir($dh))!== false){
				if ($file != '.' && $file != '..') {
					if(file_exists("{$dir}/{$file}")) @unlink("{$dir}/{$file}");
				}
			}
			closedir($dh);
		}
	}

	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;

		if ($this->cache) {
			$md5 = md5_file($file);
			$dir = $this->cacheDir;
			if (file_exists("{$dir}/{$md5}")) {
				$this->cacheLoad = true;
				$this->resource = file_get_contents("{$dir}/{$md5}");
			}else{
				$this->cacheLoad = false;
				$this->resource = file_get_contents($file);
			}
		}else{
			$this->resource = file_get_contents($file);
		}
		return true;
	}

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

	public function tplFetch($display = false) {
		/*
		 * cache mode
		 *
		 */
		if ($this->cache && $this->cacheLoad) {
			if ($display) {
				echo $this->resource;
				return;
			}else{
				return $this->resource;
			}
		}

		/*
		 * 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);

		/*
		 * cache mode
		 *
		 */
		if ($this->cache) {
			$md5 = md5_file($this->template);
			$dir = $this->cacheDir;
			file_put_contents("{$dir}/{$md5}",$this->resource);
		}

		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行目
    PHPの開始タグです。
    このプログラムコードですが、開始タグに対して終了タグがありません。
    通常は終了タグも書くのですが、終了タグを書くことによる弊害というのがあってここでは終了タグを書いていません。
    詳しくはPHP ManualのPHPタグの箇所にも書いていますのでご一読ください。
  • 2行目~10行目
    コメント行です。
    オープンソースに限らず、プログラムを書くときは著作者名や作成日、バージョン情報などを書いておく癖を付けると後々管理が楽です
  • 12行目
    templateクラスの宣言部分
  • 13行目~16行目
    変数の初期化。この辺は以前と変わりなしです。
  • 17行目~42行目
    コンストラクトメソッドの部分は28行目~30行目に新たな変数を追加しました。
    それぞれの役割は後で説明します。
  • 43行目~47行目
    デストラクトメソッドの部分は以前と変わらず処理なしです。
  • 49行目~51行目
    新たにcacheModeメソッドを追加しました。
    このメソッドでキャッシュのON/OFFを切り替えます。
    キャッシュの状態を記憶するために$this->cacheMode変数を使用します。
  • 53行目~55行目
    新たにcacheDirメソッドを追加しました。
    このメソッドでキャッシュの保存先を指定します。
    キャッシュディレクトリを記憶するために$this->cacheDir変数を使用します。
  • 57行目~67行目
    新たにcacheClearメソッドを追加しました。
    このメソッドでキャッシュの削除を行います。
    ※ユーザーが手動で(プログラム制御で)キャッシュを削除することが出来ます。
    処理の内容は、キャッシュディレクトリをチェックして、存在していたらキャッシュディレクトリ内のファイルを1件づつ削除するようになっています。
  • 69行目~76行目
    前回と同様の処理内容です。
  • 78行目~96行目
    tplSetupメソッドでは、キャッシュが有効の場合の処理を追加しました。
    82行目でキャッシュが有効かどうかを判別し、有効であれば83行目~91行目の処理を行います。
    83行目~91行目の処理は以下の通りです。
    • 83行目
      md5_file()関数でテンプレートファイルのMD5値を取得
    • 84行目
      キャッシュディレクトリを取得
    • 85行目
      キャッシュディレクトリ内に上記「MD5値をファイル名に持っている」ファイルがあるか調べる
    • 86行目~87行目
      あれば、$this->cacheLoad変数にtrue(キャッシュ読み込み完了)をセットし、キャッシュファイルを読み込む。
    • 88行目~90行目
      キャッシュディレクトリにキャッシュファイルが無ければ、テンプレートファイルを読み込む
    キャッシュが無効の場合は92行目~94行目の処理を行います。(テンプレートファイルを読み込む)
  • 98行目~100行目
    前回と同様の処理内容です。
  • 102行目~218行目
    tplFetchメソッドもキャッシュ処理のために処理を追加しました。
    103行目~114行目では、キャッシュが有効で、なお且つキャッシュファイルを読み込んでいる場合(tplSetupメソッドでキャッシュファイルを読み込んだ場合)、そのファイルを返り値として処理します。
    ※キャッシュが有効で、なお且つキャッシュファイルがある場合は以降の処理を行わないため高速化することが出来ます。
    キャッシュが無効、またはキャッシュファイルがまだ作成されていない場合は以後の処理に続きます。
    201行目までは従来通りの処理なので、説明は省略します。
    203行目~211行目までが、新たに追加した処理です。
    203行目~211行目ではキャッシュが有効だった場合にコンパイルしたデータ(PHPコードに置換したテンプレートファイル)をmd5_file()関数を使ってMD5値を取得して、その値をファイル名としてキャッシュディレクトリに保存します。
    md5_file()関数は、ファイルの中身が変わればMD5値も変わるので、この値をファイル名にすることで、テンプレートファイルが変更されたかどうかを安易に比較することが出来ます。
    ファイルの保存には< ahref="http://php.net/manual/ja/function.file-put-contents.php">file_put_contents()関数を使用します。
    これでキャッシュファイルの保存は完了です。
  • 220行目~最後まで
    以前と同様の処理なので省略

これで、キャッシュが完成しました。
正直このキャッシュは最低限のことしかしていませんので、改良する余地は(たくさん)あります。
例えば、複数のテンプレートファイルを処理する場合に、cacheClearメソッドを使用すると全部のキャッシュが削除されてしまうので、個別に削除する方法を実装するとか。
例えば、キャッシュの有効期限を作って有効期限切れのキャッシュは自動で削除されるとか
ほかにも色々とアイディアはあると思いますので、一度ご自身で考えてみて実装してみてください。
「こんなの作ったよ」とか教えてもらえると超うれしいですので、その時はご連絡くださいませ。

まとめ

PHPに限らず、プログラミングは構文や関数を覚えることも大事ですが、それ以上に「処理の流れ」を考える力を付けることが大事です。
「処理の流れ」を考える事が出来れば、どんな開発言語でも作れます!(ある程度はw)
プログラミングを続ければ自然に考える力は身に付きます!(程度差はありますがw)
なので、これからもぜひプログラミングを続けてみてください(^-^)

今回覚えた関数

コメントをどうぞ!