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

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

今プロPHPも10回目を迎えました!やったー!
毎週更新というのがこれほど大変だとは思ってもいませんでしたが、頑張って続けていきますね!

では、今回も張り切って行きましょう。今プロPHP第10回始めますー


今回は、前回のソースコードtemplate.class.phpの中身を見ていくところから始めます。
では、コードを見ていきましょう。

template.class.phpの中身

<?php
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();
		/*
		 * 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 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("/\{for[ ]+?(.+?)[ ]+?from[ ]+?(.+?)to[ ]+?(.+?)[ ]+?step[ ]+?(.+?)\}/", "<?php for (\\1 = \\2; \\1 <= \\3; \\4) { ?>", $dat);
		$dat = preg_replace("/\{\/for\}/", "<?php } ?>", $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, $evalMode = true){
		/*
		 * variable assign
		 */
		foreach ($this->assignVal as $key=>$val) {
			$$key = $val;
		}
		if ($evalMode) {
			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{
			$filename = tempnam('/tmp/','bx');
			file_put_contents($filename,$text);
			include $filename;
			unlink($filename);
		}
		return $contents;
	}
}

中身の解説

  • 1行目、2行目
    PHPの開始タグと、template classの宣言部分です。
    そういえば、昔はPHPの開始タグは ※詳しくはHTMLからの脱出を参照してください。
  • 3行目~6行目
    クラス内で使用する変数を宣言しています。
    クラスの外では使わないのでprotectedを指定しています。
    $templateはテンプレートファイルのディレクトリパスを収容します。
    $resourceはテンプレートファイルの中身を収容します。
    $assignValは置換する文字列を収容します。
    $safePhpCodeはテンプレートファイルの中で使用可能なPHPコードを収容します。
  • 10行目
    コンストラクタメソッド(__construct)でテンプレートエンジンの初期化を行います。
  • 14行目~16行目
    3行目~6行目で定義した変数を初期化します。
  • 21行目
    get_defined_functions()関数を使って定義済みの関数をすべて取得します。
    ※この関数はユーザー関数も取得してくれます。
  • 22行目
    get_defined_functions()関数で取得した値は大きく分けて2つ(internalとuser)に分かれて配列に収容されます。 ※internalはPHPが定義している関数、userはユーザー関数です。 ここでは、array_merge()関数を使ってinternalとuserの中にある値(配列)を1つにくっつけて$this->funcsに収容しています。
  • 23行目
    unset()関数を使って$funcsを破棄しています。
    $funcsと$this->funcsは別の変数です。
  • 24行目
    $safePhpCode変数に許可したいPHP関数を配列で収容しています。
  • 25行目~27行目
    foreach文で$this->funcs変数内の定義済み関数名(禁止したいPHP関数)から、$safePhpCode内のPHP関数名(許可したいPHP関数)をunset関数で取り除きます。
  • 29行目~33行目
    デストラクトメソッド(__destruct)では、クラスの終了時に処理する内容を登録しますが、今回は使用しないので、関数内は空です。
  • 34行目~36行目
    tplAssignメソッドで置換す対文字列の処理を登録します。
    $this->assignVal変数に「置換するキー=$tplVal」「置換する値=$value」の形で登録しています。
  • 37行目~42行目
    tplSetupメソッドでテンプレートファイルを読み込んで変数に収容します。
    38行目では、file_exists()関数でファイルが存在しない場合、false値(0)を返します。
  • 43行目~45行目
    tplDisplayメソッドは、テンプレートファイルを出力する為の関数です。
    46行目のtplFetchメソッドを引数(true)付きで呼び出しています。
  • 46行目
    tplFetchメソッドは引数の値がfalseの場合は、テンプレートファイルを文字列で返します。
    trueの場合は、画面出力を行います。
  • 51行目~54行目
    ここでは、テンプレートファイルの中にPHPの開始タグが無いか調べています。
    PHP開始タグがあった場合はエラーを出力して終了します。
    ※PHP開始タグでショートタグが有効な場合は上記処理以外にも制限を加えた方がより安全です。
  • 58行目~70行目
    ここでは、テンプレートエンジンの独自タグ「IF」の評価式の中に使用できるPHP関数をチェックして、$this->funcs変数に登録されている関数があった場合はエラーを出力して終了します。
    処理の詳細は、テンプレートファイル内の if タグを調べて$matches変数に収納(59行目)し、
    $matches変数に値が収納されていれば(if タグがあったら)if タグ内の評価式を順にチェックします。
    62行目ではpreg_replace関数で余分な空白やタブ、改行を取り除いています。
    63行目で$this->funcs変数(禁止するPHP関数)を順に呼び出して64行目で禁止する関数を調べます。
    65行目・66行目で、禁止する関数が見つかった場合の処理(エラーを出力して終了)をしています。
  • 77行目~80行目
    ここでは、if タグをPHP関数の処理に置き換えます。
    最初だけ(77行目)$this->resourceから値を取得して、$dat変数に代入しています。
    $this->resourceの中身は変更したくないので $dat変数に処理結果を入れています。
    78行目以降は$dat変数を使って置換処理を進めていきます。
  • 87行目~89行目
    foreach タグをPHP関数の処理に置き換えます。
    foreach タグではキーを使う場合と、使わない場合の2通りのパターンで置換処理をしています。
    {foreach $data as $key=>$val}


    {/foreach}
    の場合と
    {foreach $data as $val}


    {/foreach}
    の場合です。
  • 95行目~96行目
    新しくfor タグも作りました。
    for タグは以下のような形で動きます。
    {for $data from 0 to 100 step 1}


    {/for}
  • 104行目~110行目
    ここでは、テンプレートエンジンのタグで変数の加減算、変数の代入などができるようにしました。
    104行目は変数 基本的な事に書かれている有効な変数名を正規表現で表現した結果を$varParam変数に代入しています。
    105行目~110行目まで、104行目で定義した有効な変数名を使って、変数の加減算・代入処理をPHPの処理に置換しています。
  • 114行目
    ここではテンプレートエンジンタグの変数を、出力する(echo文で表示する)処理に置換しています。
  • 116行目
    テンプレートファイルの置換処理(PHP関数に変換)が終わったデータをIncludeTextメソッドに渡します。
  • 117行目~121行目
    ここでは、46行目の$display変数の値(直接出力するか、文字列を返すか)を判別して処理しています。
  • 123行目
    IncludeTextメソッドはクラス内でのみ使用する関数なので、privateで宣言しています。
    また、引数の2番目はincludeを使うかevalを使うか指定できるようにしています(デフォルトはevalです)
  • 127行目~129行目
    ここでは、$this->assignVal変数に登録された置換する文字列を、処理しています。
    $$key=$val;
    とすることで、$keyに入っている値を変数名として、$valの値を代入します。

    $key = 'hoge';
    $$key = 'test';
    とした場合
    $hoge = 'test';
    と同じ処理になります。
  • 130行目~146行目
    この処理は以前学んだ内容と同じなので、ざっとだけ説明しますね。
    130行目は処理方法の判別です。evalを使った処理なら131行目からの処理、includeを使った処理なら142行目からの処理になります。
    131行目ではob_startを使って、出力をキャッシュして表示させないようにしています。
    try~catch文でエラー制御をして、エラーが出たらエラーメッセージを表示して終了しています。
    また、エラーが無い場合はob_get_contents()関数を使って、出力結果を$contents変数に代入しています。
    142行目~のincludeを使った処理では、置換処理を済ましたデータが入った一時ファイルを作成して、include文で呼び出しています。
    最後に$contents変数を返して処理が終了です。

最後に

長いですね。
あまりに長いので、読んでても頭に入らないかもしれませんw
では、ちょっと頭を働かすため(?)に一言。
このクラスは、123行目のIncludeTextメソッドで $evalMode=false に変更すると思ったような処理をしてくれませんw
※実際には、tplDisplay()を呼んでも、tplFetch()を呼んでも出力されてしまいます。
さて、何故でしょうか?というのが、次回に繋げるネタですw

  • ヒント1:141行目~の処理が原因です(処理が足りません)。
  • ヒント2:131行目のevalでは使っています。

というわけで、次回に続きます~。

この記事はコメントの受付をしていません。