viewDir = rtrim($viewDir ?? ROOT.'/view', '/'); $this->tmplPath = rtrim($tmplPath, '/'); if (substr($this->tmplPath, 0, 1) !== '/') { $this->tmplPath = '/'.$this->tmplPath; } } /** * テンプレート変数に値を割り当てる * * @param string $name 変数名 * @param mixed $value 値 * @return void */ public function assign(string $name, mixed $value): void { $this->vars[$name] = $value; } /** * カスタムCSSファイルを追加する * * @param string $name CSSファイル名 * @return void */ public function addCss(string $name): void { $this->custCss[] = '\n"; $this->assign('custCss', $this->custCss); } /** * カスタム関数を登録する * * @param string $name 関数名 * @param callable $callback コールバック関数 * @return void */ public function registerFunction(string $name, callable $callback): void { $this->custFunc[$name] = $callback; } /** * テンプレートブロックを定義する * * @param string $name ブロック名 * @param string $content ブロック内容 * @return void */ public function defineBlock(string $name, string $content): void { if (!isset($this->blocks[$name])) $this->blocks[$name] = $content; } /** * テンプレートをレンダリングする * * @param string $tmplName テンプレート名 * @return void */ public function render(string $tmplName): void { $tmplPath = $this->viewDir.$this->tmplPath.'/'.$tmplName.$this->tmplExt; if (!file_exists($tmplPath)) throw new \RuntimeException("テンプレートファイルを見つけません:{$tmplPath}"); $content = file_get_contents($tmplPath); // インクルードディレクティブを処理(無限ループにならない様に回数を制限する) $depth = 0; while (preg_match('/\{@\s*include\((.*?)\)\s*@\}/s', $content)) { if (++$depth > 32) throw new \RuntimeException("インクルードが深過ぎます(循環?):{$tmplPath}"); $content = preg_replace_callback('/\{@\s*include\((.*?)\)\s*@\}/s', function($m): bool|string { $inclPath = $this->viewDir.'/'.trim($m[1], "'\" ").$this->tmplExt; if (!file_exists($inclPath)) throw new \RuntimeException("ファイルを見つけません: {$inclPath}"); return file_get_contents($inclPath); }, $content); } $content = $this->procComments($content); $content = $this->procDirs($content); $content = $this->procVars($content); $content = $this->procFuncs($content); $tmpFile = tempnam(sys_get_temp_dir(), 'tmpl_'); file_put_contents($tmpFile, $content); try { // 別のスコープで実行する。以前はextract()がこのメソッドの$contentや$tmplPathを上書き出来た // (「content」という名前の変数を割り当てると、テンプレートのソースが表示された) (function() { extract(func_get_arg(1)); include func_get_arg(0); })($tmpFile, $this->vars); } finally { unlink($tmpFile); } } /** * テンプレートをレンダリングして、文字列として返す(メール本文等に) * * @param string $tmplName テンプレート名 * @return string レンダリングされた内容 */ public function fetch(string $tmplName): string { ob_start(); try { $this->render($tmplName); return (string)ob_get_contents(); } finally { ob_end_clean(); } } // 機能性メソッド /** * コメント({# … #})を取り除く * 他の構文より先に処理するので、コメントの中の{{ }}等は実行されない * * @param string $content テンプレート内容 * @return string 処理後の内容 */ private function procComments(string $content): string { return preg_replace('/\{#.*?#\}/s', '', $content); } /** * テンプレートディレクティブを処理する * * @param string $content テンプレート内容 * @return string|null 処理後の内容 */ private function procDirs(string $content): string|null { // includeディレクティブの処理 while (preg_match('/\{@\s*include\((.*?)\)\s*@\}/s', $content)) { $content = preg_replace_callback('/\{@\s*include\((.*?)\)\s*@\}/s', function($m): bool|string { $inclPath = $this->viewDir.'/'.trim($m[1], "'\" ").'.php'; if (!file_exists($inclPath)) throw new \RuntimeException("ファイルを見つけません: {$inclPath}"); return file_get_contents($inclPath); }, $content); } $content = preg_replace('/\{@\s*if\s*\((.*?)\):\s*@\}/', '{@ if ($1) @}', $content); $content = preg_replace('/\{@\s*endif;\s*@\}/', '{@ endif @}', $content); $processDirectives = function($c): array|string|null { // kysディレクティブの処理 $c = preg_replace_callback('/\{@\s*kys\((.*?)\)\s*@\}/s', function($m): string { return "'; print_r({$m[1]}); echo ''; die(); ?>"; }, $c); // foreachループとネストした内容の処理 $c = preg_replace_callback('/\{@\s*foreach\s*\((.*?)\)\s*@\}/s', function($m): string { return ""; }, $c); $c = preg_replace_callback('/\{@\s*endforeach\s*@\}/s', function($m): string { return ""; }, $c); // forループの処理 $c = preg_replace_callback('/\{@\s*for\s*\((.*?)\)\s*@\}/s', function($m): string { return ""; }, $c); $c = preg_replace_callback('/\{@\s*endfor\s*@\}/s', function($m): string { return ""; }, $c); // if-elif-else-endifの処理 $c = preg_replace_callback('/\{@\s*if\s*\((.*?)\)\s*@\}/s', function($m): string { return ""; }, $c); $c = preg_replace_callback('/\{@\s*elif\s*\((.*?)\)\s*@\}/s', function($m): string { return ""; }, $c); $c = preg_replace_callback('/\{@\s*else\s*@\}/s', function($m): string { return ""; }, $c); $c = preg_replace_callback('/\{@\s*endif\s*@\}/s', function($m): string { return ""; }, $c); return $c; }; $previousContent = ''; $maxIterations = 10; $iterations = 0; while ($previousContent !== $content && $iterations < $maxIterations) { $previousContent = $content; $content = $processDirectives($content); $iterations++; } return $content; } /** * テンプレート変数を処理する * * @param string $content テンプレート内容 * @return string 処理後の内容 */ private function procVars(string $content): string { // 変数の出力(エスケープ処理なし) $content = preg_replace_callback('/\{\{\{\s*(.*?)\s*\}\}\}/', function($m): string { return ''; }, $content); // 変数の出力(エスケープ処理あり) $content = preg_replace_callback('/\{\{\s*(.*?)\s*\}\}/', function($m): string { return ''; }, $content); // 変数の代入 $content = preg_replace_callback('/\{\$\s*(.*?)\s*\$\}/', function($m): string { $parts = explode('=', $m[1], 2); if (count($parts) !== 2) throw new \RuntimeException("不正な値の形式"); return ''; }, $content); // PHPコードの実行 $content = preg_replace_callback('/\{\!\s*(.*?)\s*\!\}/', function($m): string { return ''; }, $content); return $content; } /** * カスタム関数を処理する * * @param string $content テンプレート内容 * @return string 処理後の内容 */ private function procFuncs(string $content): string { foreach ($this->custFunc as $name => $cb) { $pattern = "/\{@\s*{$name}\((.*?)\)\s*@\}/"; $content = preg_replace_callback($pattern, function($m) use ($cb): mixed { $args = explode(',', $m[1]); $args = array_map('trim', $args); return call_user_func_array($cb, $args); }, $content); } return $content; } }