レポジトリ種類: SVN
<?php
namespace Site\Lib;
class Template {
private string $tmplExt = '.maron';
private string $tmplPath;
private array $vars = [];
private array $blocks = [];
private array $custFunc = [];
private array $custCss = [];
/**
* テンプレートクラスのコンストラクタ
*
* @param string $tmplPath テンプレートのパス
* @return void
*/
public function __construct(string $tmplPath) {
$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[] =
'<link rel="stylesheet" type="text/css" href="/static/style-'.$name.'.css" />';
$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 = ROOT.'/view'.$this->tmplPath.'/'.$tmplName.$this->tmplExt;
if (!file_exists($tmplPath))
throw new \RuntimeException("テンプレートファイルを見つけません:{$tmplPath}");
extract($this->vars);
$content = file_get_contents($tmplPath);
// インクルードディレクティブを処理
while (preg_match('/\{@\s*include\((.*?)\)\s*@\}/s', $content)) {
$content = preg_replace_callback('/\{@\s*include\((.*?)\)\s*@\}/s', function($m) {
$inclPath = ROOT.'/view/'.trim($m[1], "'\" ").$this->tmplExt;
if (!file_exists($inclPath))
throw new \RuntimeException("ファイルを見つけません: {$inclPath}");
return file_get_contents($inclPath);
}, $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);
include $tmpFile;
unlink($tmpFile);
}
// 機能性メソッド
/**
* テンプレートディレクティブを処理する
*
* @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) {
$inclPath = ROOT.'/view/'.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) {
// kysディレクティブの処理
$c = preg_replace_callback('/\{@\s*kys\((.*?)\)\s*@\}/s', function($m) {
return "<?php echo '<pre>'; print_r({$m[1]}); echo '</pre>'; die(); ?>";
}, $c);
// foreachループとネストした内容の処理
$c = preg_replace_callback('/\{@\s*foreach\s*\((.*?)\)\s*@\}/s', function($m) {
return "<?php foreach({$m[1]}): ?>";
}, $c);
$c = preg_replace_callback('/\{@\s*endforeach\s*@\}/s', function($m) {
return "<?php endforeach; ?>";
}, $c);
// forループの処理
$c = preg_replace_callback('/\{@\s*for\s*\((.*?)\)\s*@\}/s', function($m) {
return "<?php for({$m[1]}): ?>";
}, $c);
$c = preg_replace_callback('/\{@\s*endfor\s*@\}/s', function($m) {
return "<?php endfor; ?>";
}, $c);
// if-elif-else-endifの処理
$c = preg_replace_callback('/\{@\s*if\s*\((.*?)\)\s*@\}/s', function($m) {
return "<?php if ({$m[1]}): ?>";
}, $c);
$c = preg_replace_callback('/\{@\s*elif\s*\((.*?)\)\s*@\}/s', function($m) {
return "<?php elseif ({$m[1]}): ?>";
}, $c);
$c = preg_replace_callback('/\{@\s*else\s*@\}/s', function($m) {
return "<?php else: ?>";
}, $c);
$c = preg_replace_callback('/\{@\s*endif\s*@\}/s', function($m) {
return "<?php endif; ?>";
}, $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) {
return '<?= '.trim($m[1]).' ?>';
}, $content);
// 変数の出力(エスケープ処理あり)
$content = preg_replace_callback('/\{\{\s*(.*?)\s*\}\}/', function($m) {
return '<?= htmlspecialchars('.trim($m[1]).', ENT_QUOTES, \'UTF-8\') ?>';
}, $content);
// 変数の代入
$content = preg_replace_callback('/\{\$\s*(.*?)\s*\$\}/', function($m) {
$parts = explode('=', $m[1], 2);
if (count($parts) !== 2)
throw new \RuntimeException("不正な値の形式");
return '<?php '.trim($parts[0]).' = '.trim($parts[1]).'; ?>';
}, $content);
// コメント
$content = preg_replace_callback('/\{#\s*(.*?)\s*#\}/', function($m) {
return '<?php /*'.trim($m[1]).'*/ ?>';
}, $content);
// PHPコードの実行
$content = preg_replace_callback('/\{\!\s*(.*?)\s*\!\}/', function($m) {
return '<?php '.trim($m[1]).' ?>';
}, $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) {
$args = explode(',', $m[1]);
$args = array_map('trim', $args);
return call_user_func_array($cb, $args);
}, $content);
}
return $content;
}
}