php ムスタッシュ風テンプレートエンジン 自作
参考
https://qiita.com/tak-solder/items/1718cc91daefad41efed
https://qiita.com/tak-solder/items/87bc4dd4803654c0c84a
やりたいこと
- ムスタッシュはよく見るので、そんな感じをイメージして実装。
- phpテンプレートのように使う。
- 拡張子はムスタッシュのmを2回重ねて、
index.mm.php
のようにする。 - cacheも使えるようにする。
- 特定のphpキーワードを実行可能。
フォルダ構成
- / (ルート)
index.php
<?php declare(strict_types=1); ini_set('display_errors', '1'); error_reporting(-1); include __DIR__ . '/ViewMustache.php'; const VIEW_DIR = __DIR__ . '/views/'; const CACHE_DIR = __DIR__ . '/cache/'; $view = new ViewMustache(VIEW_DIR, CACHE_DIR); echo $view->render('index', [ 'title' => 'Articles', 'articles' => [ 'article1', 'article2', 'article3', ], 'hoge' => 0, 'base_url' => 'http://example.com' ]);
ViewMustache.php
<?php declare(strict_types=1); /** * @link https://qiita.com/tak-solder/items/1718cc91daefad41efed * @link https://qiita.com/tak-solder/items/87bc4dd4803654c0c84a */ class ViewMustache { private $view_dir = ''; private $cache_dir =''; private $param = []; private $use_cache = true; private const MUSTACHE_EXTENSION = '.mm.php'; private const PHP_KEYWORD = [ 'require', 'foreach', 'endforeach', 'if', 'elseif', 'else', 'endif', ]; public function __construct(string $view_dir, string $cache_dir) { $this->view_dir = $view_dir; $this->cache_dir = $cache_dir; } public function render(string $file_name, array $param = []): string { $this->param = array_merge($this->param, $param); $view = $this->makeCache($file_name); extract($this->param); ob_start(); ob_implicit_flush(0); require $view; return ob_get_clean(); } private function searchPHPKeyword($word): bool { return preg_match('/^(' . join('|', self::PHP_KEYWORD) . ')/', $word) === 1; } /** * sha1_file()でファイルのハッシュ値の計算をし、 * テンプレートファイルが更新される毎に値が変わるので、 * 変更された場合、新規のキャッシュファイルが生成される。 */ private function makeCache(string $file_name): string { $view_file = $this->view_dir . $file_name . self::MUSTACHE_EXTENSION; $cache_file = $this->cache_dir . sha1_file($view_file); if ($this->checkCache($view_file, $cache_file)) { return $cache_file; } $this->write($view_file, $cache_file); return $cache_file; } private function write(string $view_file, string $cache_file) { $source = file_get_contents($view_file); // 改行削除 $source = str_replace(PHP_EOL, '', $source); // ムスタッシュキーワード検索 $source = preg_replace_callback('#\{\{(.*?)\}\}#', function ($m) { $v = trim($m[1]); // 予約語なら命令を実行する if ($this->searchPHPKeyword($v)) { // requireの場合、親ファイル呼び出し if (strpos($v, 'require') === 0) { $file = preg_split("/\s/", $v)[1]; $file = trim($file, "\'\""); return $this->render($file); } return '<?php ' . $v . ' ?>'; } // そうでないなら変数として扱う return '<?= ' . $v . ' ?>'; }, $source); file_put_contents($cache_file, $source, LOCK_EX); } /** * 下記の条件が全てtrueの場合のみキャッシュを使う * * requireで呼び出しているファイルのキャッシュが存在する * 現在のビューのキャッシュファイルが存在する * キャッシュを使う */ private function checkCache(string $view_file, string $cache_file): bool { if (!$this->checkRequireCache($view_file)) { return false; } if (!is_file($cache_file)) { return false; } if (!$this->use_cache) { return false; } return true; } /** * require命令から呼び出されているファイルのキャッシュを確認する */ private function checkRequireCache(string $file_name): bool { $source = file_get_contents($file_name); preg_match_all('#\{\{(.*require .*)\}\}#', $source, $match_list); $requires = $match_list[1]; foreach ($requires as $require) { $str = preg_split("/\s/", $require)[2]; $require_file = trim($str, "\"\'"); $cache_file = $this->cache_dir . sha1_file($this->view_dir . $require_file . self::MUSTACHE_EXTENSION); if (!is_file($cache_file)) { return false; } } return true; } }
index.mm.php
<h2>Hello Index.php {{ $title }}</h2> {{ require 'layout/dummy' }} {{ require 'layout/base' }} <h2>{{ $title }}</h2> <ul> {{ foreach ($articles as $article): }} <li>{{ $article }}</li> {{ endforeach; }} </ul> {{ if ($hoge === 0): }} <p>hoge = 0</p> {{ elseif ($hoge === 1): }} <p>hoge = 1</p> {{ else: }} <p>hoge = else</p> {{ endif; }}
dummy.mm.php
<h3>++++ dummy ++++</h3>
base.mm.php
<h3>++++ BASE ++++</h3> <p>{{ $base_url }}</p>
出力 phpテンプレート
<h2>Hello Index.php <?= $title ?></h2> <h3>++++ dummy ++++</h3> <h3>++++ BASE ++++</h3> <p>http://example.com</p> <h2><?= $title ?></h2> <ul> <?php foreach ($articles as $article): ?> <li><?= $article ?></li> <?php endforeach; ?> </ul> <?php if ($hoge === 0): ?> <p>hoge = 0</p> <?php elseif ($hoge === 1): ?> <p>hoge = 1</p> <?php else: ?> <p>hoge = else</p> <?php endif; ?>