LoginSignup
0
0

More than 5 years have passed since last update.

PHPのテンプレートライブラリplatesのレンダリング処理を読む

Posted at

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;
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0