シンプルなテンプレートエンジンPlatesをSilexで使ってみる

Platesという、シンプルなテンプレートエンジンを使ってみた。 小さなWebアプリケーションでTwigが必要なほどではない場合にはちょうど良いと思う。

platesphp.com

まずはSilexで最低限のものを実装しよう。

{
    "require": {
        "silex/silex": "~1.3"
    }
}
<?php
// web/index.php
require_once __DIR__ .'/../vendor/autoload.php';

$app = new Silex\Application();

$app->get('/', function () {
    return "Hello";
});
$app->run();

Getting Started

composer.jsonにPlatesを追加してcomposer updateする。

{
    "require": {
        "silex/silex": "~1.3",
        "league/plates": "3.*"
    }
}

では最低限の実装を。

<?php
// web/index.php
require_once __DIR__ .'/../vendor/autoload.php';

$app = new Silex\Application();

$app->get('/', function () {
    $templates = new League\Plates\Engine(__DIR__ . '/../templates');
    return $templates->render('home', ['title' => 'Hello']);
});
$app->run();
<!-- templates/home.php -->
<h1><?=$this->e($title)?></h1>

見ての通り、テンプレートエンジンというよりほぼPHPそのままだ(まあそもそもPHPってそういうものなんだけど)。 PHPにテンプレートエンジンとして使うための便利機能を追加したもの、と考えた方がいいかもしれない。

ルートのアクション内でEngineをnewするのはあんまりなので改善しよう。行儀よくやるならServiceProviderを使うのかもしれないけどここはより単純に、単にコンテナに追加してしまおう。

<?php
// web/index.php
...
$app['view'] = $app->share(function () {
    return new League\Plates\Engine(__DIR__ . '/../templates');
});

$app->get('/', function () use ($app) {
    return $app['view']->render('home', ['title' => 'Hello']);
});

Silexのバージョンは1.3.0で、Pimpleが1.1.1なので$app->share()を使っている。

layoutとinsert

もう少し色々試してみたいんだけど、ネタがないと難しいのでRails Tutorialの静的ページ部分を借りてくることにした。詳細は省くがnpmでbootstrap-sassをインストールし、gruntでビルドできるようにした。

さてレイアウトを導入しよう。

<!-- templates/home.php -->
<?php $this->layout('layouts/application', ['title' => 'Home']) ?>

<div class="center hero-unit">
    <h1>Welcome to the Sample App</h1>

    <h2>
        This is the home page for the
        <a href="http://railstutorial.jp/">Ruby on Rails Tutorial</a>
        sample application.
    </h2>
</div>
<!DOCTYPE html>
<!-- templates/layouts/application.php -->
<html>
<head>
    <title><?=$title?></title>
    <link rel="stylesheet" type="text/css" href="app.css">
    <?=$this->fetch('layouts/shim')?>
</head>
<body>
<?=$this->fetch('layouts/header')?>
<div class="container">
    <?=$this->section('content')?>
    <?=$this->fetch('layouts/footer')?>
</div>
</body>
</html>

$this->layout()でレイアウトを指定し、$this->section()で呼び出している。$this->section()の引数はセクション名で、contentは全ての出力を表す唯一の予約済みのセクション名だ。 明示的にセクションを作る場合は $this->start('name') ... $this->stop() を使う。

$this->layout()でレイアウトを指定すると同時にtitleも渡しているので、index.php側からはtitleを渡す必要は無くなった。

$app->get('/', function () use ($app) {
    return $app['view']->render('home');
});

<?=$this->fetch('layouts/shim')?><?php $this->insert('layouts/shim')?>と書くこともできる。

full_titleヘルパー関数

手順は前後するけど、Rails Tutorialの中にfull_titleというヘルパー関数が出てくるのでこれを実装してみよう。

<?php
// web/index.php
...
$app['view'] = $app->share(function () {
    $engine = new League\Plates\Engine(__DIR__ . '/../templates');
    $engine->registerFunction('full_title', function ($title) {
        $base_title = 'Ruby on Rails Tutorial';
        if (empty($title)) {
            return $base_title;
        }
        return "$base_title | $title";
    });
    return $engine;
});
<!-- templates/layouts/application.php -->
<html>
<head>
    <title><?=$this->full_title($title)?></title>
    <link rel="stylesheet" type="text/css" href="app.css">
    <?=$this->fetch('layouts/shim')?>
</head>
...

以下のようにtitleに空文字以外を渡すと、Ruby on Rails Tutorial | Home と表示され、

<!-- templates/home.php -->
<?php $this->layout('layouts/application', ['title' => 'Home']) ?>

titleに空文字を渡すと、Ruby on Rails Tutorial と表示される。

<!-- templates/home.php -->
<?php $this->layout('layouts/application', ['title' => '']) ?>

ところで、titleはエスケープして出力したほうが良いだろう。つまりfull_titleを通してからescapeすることになる。 これにはいくつかやり方がある。

<?=$this->e($this->full_title($title))?>
<?=$this->batch($title, 'full_title|e')?>
<?=$this->e($title, 'full_title')?>

1つ目は説明するまでもないだろう。2つ目の $this->batch() は、第二引数に複数のヘルパー関数名を | で繋いで記述することができ、 これらが左から順番に実行されるというもの。3つ目も同じような意味で、$this->e() 関数のみ、$this->chan() と同様に 第二引数に複数の関数名を| で繋いで記述することができ、そして最後にエスケープが行われる。

titleのデフォルト値

またちょっと戻るのだけれど、titleを表示したくないときに、

<!-- templates/home.php -->
<?php $this->layout('layouts/application', ['title' => '']) ?>

とやるのはちょっとカッコ悪い。しかしなにも渡さないと Undefined variable になってしまう。 レイアウト側でデフォルトを指定する方法も無くはなさそうだけど不格好なので、 Engine 側でデフォルト値を指定してしまおう。

<?php
// web/index.php
...
$app['view'] = $app->share(function () {
    $engine = new League\Plates\Engine(__DIR__ . '/../templates');
    $engine->addData(['title' => '']);
...

こうすると、全てのテンプレートで title に空文字列がセットされるので、home.php側からはtitleを渡す必要がなくなった。

<!-- templates/home.php -->
<?php $this->layout('layouts/application') ?>