PHPのテンプレートライブラリであるthephpleague/plates
のソースコードを読みました。とてもシンプルな実装で、テンプレートは生のPHPを書きます。この記事では読みやすいようにコードを並び替えて記載しています。
Engineから各クラスに委譲をしており、委譲先のクラスは小さいです。移譲と分割の参考になると思います。継承はExtensionInterface以外では使われておりません。
テンプレートファイルのレンダリング
下記のコードがこのライブラリのメインとなる処理です。ここを見ていきます。
// pathはEngineに委譲したDirectoryで管理されます。
// Directoryはpathを保持するだけのクラスで、pathはただの文字列として保持します。
$templates = new League\Plates\Engine('/path/to/templates');
// $templatesはEngineであることに注意です。renderを通じて、Engineに委譲したTemplateのrenderを実行します。
// profileのTemplateを生成して、その場でrenderを実行するということです。
echo $templates->render('profile', ['name' => 'Jonathan']);
Engineの生成
Engine以外のクラスのインスタンスを持ちます。委譲されています。小さく分割されたクラスのオブジェクト(インスタンス)を組み合わせてEngineは作られています。他のクラスをEngineのAPI(メソッド)にまとめています。
make()はTemplateを生成するだけで、render()になるとmake()で生成してすぐにレンダリングがされます。
// Engine.php
public function __construct($directory = null, $fileExtension = 'php')
{
$this->directory = new Directory($directory);
$this->fileExtension = new FileExtension($fileExtension);
$this->folders = new Folders();
$this->functions = new Functions();
$this->data = new Data();
}
// Engine.php
public function render($name, array $data = array())
{
return $this->make($name)->render($data);
}
// Engine.php
public function make($name)
{
return new Template($this, $name);
}
Templateの生成
Engineの他にロジックを持っているとしたら、このクラスです。
Template->dataプロパティはただの配列ですが、Engine->dataはdataクラスです。dataプロパティはEngineとTemplateの両方に持たせています。Engineのdataで全テンプレート共通のdataを設定することができます。デフォルト値ですね。
// Template.php
public function __construct(Engine $engine, $name)
{
$this->engine = $engine;
$this->name = new Name($engine, $name);
// Engine->addDataで、全テンプレート共通のDataをセットできます。
$this->data($this->engine->getData($name));
}
// Engine.php
public function getData($template = null)
{
// データはEngineに委譲されたDataインスタンスのgetメソッドを通す。
return $this->data->get($template);
}
// Template.php
public function data(array $data)
{
$this->data = array_merge($this->data, $data);
}
Templateのレンダリング
templateファイルはrenderメソッドの中でincludeされます。そのためtemplate内はTemplateインスタンスの中なので、escapeなど使えるメソッドはTemplateに記述されているものです。
これでテンプレートファイルが描画される処理は終わりです。
// Template.php
// layoutNameが設定されている場合は、再帰的に呼び出されます。
public function render(array $data = array())
{
try {
$this->data($data);
// $this->dataは消えません。$dataはコピーされています。
unset($data);
// 2次元配列で変数定義をします。
// このスコープでテンプレートをinluceすれば、テンプレート変数として扱えます。
extract($this->data);
ob_start();
if (!$this->exists()) {
throw new LogicException(
'The template "' . $this->name->getName() . '" could not be found at "' . $this->path() . '".'
);
}
// pathはただの文字列です。Name->getPathを実行します。
include $this->path();
$content = ob_get_clean();
// layout()で内側のテンプレートから指定した、外側のテンプレートのレンダリングを行います。
// layoutNameは、テンプレートファイルで$this->layout()を通じてセットされます。
if (isset($this->layoutName)) {
// $layoutはTemplateクラスです。
$layout = $this->engine->make($this->layoutName);
// sections['content']にテンプレートファイルの中身が入ります。
$layout->sections = array_merge($this->sections, array('content' => $content));
$content = $layout->render($this->layoutData);
}
return $content;
} catch (LogicException $e) {
// バッファにある文字数
if (ob_get_length() > 0) {
ob_end_clean();
}
throw $e;
}
}
// Template.php
public function exists()
{
return $this->name->doesPathExist();
}
// Name.php
public function doesPathExist()
{
return is_file($this->getPath());
}
// Template.php
public function path()
{
return $this->name->getPath();
}
// Name.php
public function getPath()
{
if (is_null($this->folder)) {
return $this->getDefaultDirectory() . DIRECTORY_SEPARATOR . $this->file;
}
$path = $this->folder->getPath() . DIRECTORY_SEPARATOR . $this->file;
if (!is_file($path) and $this->folder->getFallback() and is_file($this->getDefaultDirectory() . DIRECTORY_SEPARATOR . $this->file)) {
$path = $this->getDefaultDirectory() . DIRECTORY_SEPARATOR . $this->file;
}
return $path;
}
// Name.php
protected function getDefaultDirectory()
{
$directory = $this->engine->getDirectory();
if (is_null($directory)) {
throw new LogicException(
'The template name "' . $this->name . '" is not valid. '.
'The default directory has not been defined.'
);
}
return $directory;
}
Nameの生成
Nameクラスはアクセサとbooleanを返すメソッドしかありません。engine, name, folder, fileプロパティを持っています。
'dirname::template file'
という命名規則をこのクラスで表しています。Folder, Directoryクラスで委譲されています。pathは別プロパティに分けています。
// Name.php
public function __construct(Engine $engine, $name)
{
$this->setEngine($engine);
$this->setName($name);
}
// Name.php
public function setEngine(Engine $engine)
{
$this->engine = $engine;
return $this;
}
// Name.php
public function setName($name)
{
$this->name = $name;
$parts = explode('::', $this->name);
if (count($parts) === 1) {
$this->setFile($parts[0]);
} elseif (count($parts) === 2) {
$this->setFolder($parts[0]);
$this->setFile($parts[1]);
} else {
throw new LogicException(
'The template name "' . $this->name . '" is not valid. ' .
'Do not use the folder namespace separator "::" more than once.'
);
}
return $this;
}
// Name.php
// Foldersクラスのfoldersプロパティ(配列)から$folderを取得します。
public function setFolder($folder)
{
$this->folder = $this->engine->getFolders()->get($folder);
return $this;
}
// Name.php
public function setFile($file)
{
// 空文字ではない前提なので、チェックしています。関数の独立性を高めます。
if ($file === '') {
throw new LogicException(
'The template name "' . $this->name . '" is not valid. ' .
'The template name cannot be empty.'
);
}
$this->file = $file;
if (!is_null($this->engine->getFileExtension())) {
$this->file .= '.' . $this->engine->getFileExtension();
}
return $this;
}
// Engine.php
public function getFileExtension()
{
return $this->fileExtension->get();
}
データの保持するだけのクラス
どんなデータの形ごとにクラスが用意されています。目的はデータをクラスで表すだけなので、ロジックコードはありません。1つのプロパティしか持っていなくても、Engineに配列や変数で持たせずしっかりとクラス化しています。
Folders
Folderのコレクションです。add, remove, get, existsメソッドしかありません。コンストラクタも定義されていません。
Folder
name, path, fallbackプロパティを保持するだけです。アクセサはあります。
FileExtension
fileExtensionプロパティを持っているだけです。アクセサ名はget, setでプロパティ名を含んでいません。
Directory
pathプロパティを持っているだけです。FileExtensionと同じでメソッドはget, setしかありません。
命名規則
全体を表す
全体共通のテンプレート変数をentireやallではなく、sharedで表すのがいいなと思いました。
- sharedVariables
- templateVariables
複数形のクラス名
FoldersやFunctionsなど複数形で、値を束ねる意味を使っています。FolderListやFunctionListではありません。
アクセサ
引数をそのままセットしない場合は、プリフィックスsetを付けていません。下記は$dataではなく$this->dataをセットしているという感覚でしょう。
// Template.php
public function data(array $data)
{
$this->data = array_merge($this->data, $data);
}
1つしかプロパティを持たないクラスの場合は、get/setという名前にしています。
// Directory.php
public function get()
{
return $this->path;
}
isではなくdoesを使っているのは初めて見ました。
public function doesFunctionExist($name)
{
return $this->functions->exists($name);
}
参考になる書き方
どの条件にも当てはまらない場合は、最後にthrowを置きます。
// Data.php
public function add(array $data, $templates = null)
{
if (is_null($templates)) {
return $this->shareWithAll($data);
}
if (is_array($templates)) {
return $this->shareWithSome($data, $templates);
}
if (is_string($templates)) {
// 上記に合わせて文字列は配列に変換します。
return $this->shareWithSome($data, array($templates));
}
throw new LogicException(
'The templates variable must be null, an array or a string, ' . gettype($templates) . ' given.'
);
}
issetでまとめて2つの変数の存在を確認しています。&&を使う必要はありません。
// Data.php
public function get($template = null)
{
if (isset($template, $this->templateVariables[$template])) {
return array_merge($this->sharedVariables, $this->templateVariables[$template]);
}
return $this->sharedVariables;
}
Engineを通してFoldersからFolderを取ってきています。Nameに直接Foldersは持たせていません。他のオブジェクトはEngineに集約しています。
// Name.php
public function setFolder($folder)
{
$this->folder = $this->engine->getFolders()->get($folder);
return $this;
}
引数の形のチェックではなく、中身がの正常かどうかで例外を投げます。
public function remove($name)
{
if (!$this->exists($name)) {
throw new LogicException(
'The template function "' . $name . '" was not found.'
);
}
unset($this->functions[$name]);
return $this;
}