タイトル: テンプレートエンジン
SEOタイトル: PHP のテンプレートエンジン比較(Twig / Blade / Smarty / Plates / Mustache)
この本文には実行可能なタグ等が含まれるため、安全のためHTMLソースとして表示しています。本文データは変更していません。
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;background:#fffbea;border:1px solid #f4c430;margin:1em 0;width:100%"><tr><td style="background:#f4c430;color:#000;font-weight:bold;padding:8px">この記事の要点</td></tr><tr><td style="padding:10px"><ul><li>テンプレートエンジン = <strong>HTML の中に変数や制御構文を埋め込み、HTML エスケープを自動化</strong>するライブラリ</li><li>主要 5 つ: <strong>Twig</strong> (Symfony 標準・モダン) / <strong>Blade</strong> (Laravel 専用) / <strong>Smarty</strong> (老舗) / <strong>Plates</strong> (素の PHP に近い) / <strong>Mustache</strong> (ロジックレス・多言語)</li><li>共通機能: <strong>変数展開・制御構文・テンプレート継承 (extends)・include / partial・フィルタ・自動エスケープ</strong></li><li>パフォーマンス: 初回コンパイルして PHP に変換 → 以降は <strong>コンパイル済キャッシュ</strong>使用</li><li>素の PHP を使う場合は <code><?= htmlspecialchars($x, ENT_QUOTES, "UTF-8") ?></code> を毎回書く必要 → 漏れると <strong>XSS の温床</strong></li></ul></td></tr></table>
<h2>なぜテンプレートエンジンを使うのか</h2>
<p>素の PHP でも HTML を出力できますが、以下の問題があります:</p>
<ul>
<li><strong>HTML エスケープが手動</strong> → 漏れると XSS</li>
<li><strong>ロジックと表示が混在</strong>しやすく可読性が悪い</li>
<li><strong>共通レイアウトの継承</strong>が冗長(毎ファイルにヘッダー・フッターの include)</li>
<li><strong>非エンジニア(デザイナー)</strong>に PHP を触らせたくない</li>
</ul>
<p>テンプレートエンジンはこれらを<strong>制約付き構文 + 自動エスケープ</strong>で解決します。</p>
<h2>5 大テンプレートエンジンの比較</h2>
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%">
<thead><tr style="background:#e3f2fd"><th>エンジン</th><th>用途</th><th>特徴</th></tr></thead>
<tbody>
<tr><td>Twig</td><td>Symfony / フレームワーク非依存</td><td>モダン・高速・継承・サンドボックス。Drupal でも採用</td></tr>
<tr><td>Blade</td><td>Laravel 専用</td><td><code>@directive</code> 構文・コンポーネント・スロット</td></tr>
<tr><td>Smarty</td><td>老舗 (2001-)</td><td>歴史長くドキュメント豊富。記法はやや古い</td></tr>
<tr><td>Plates</td><td>素の PHP に近い</td><td>独自構文学習不要・軽量・継承サポート</td></tr>
<tr><td>Mustache</td><td>ロジックレス・多言語</td><td>JS/Ruby/Go 等にも実装あり。<code>{{var}}</code> のみ</td></tr>
</tbody>
</table>
<h2>Twig の基本</h2>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-twig">{# layout.twig - 共通レイアウト #}
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}デフォルト{% endblock %}</title>
</head>
<body>
<header>共通ヘッダー</header>
{% block content %}{% endblock %}
<footer>© {{ "now"|date("Y") }}</footer>
</body>
</html>
{# page.twig - 継承 #}
{% extends "layout.twig" %}
{% block title %}記事一覧{% endblock %}
{% block content %}
<h1>{{ pageTitle }}</h1>
{% if articles is not empty %}
<ul>
{% for a in articles %}
<li><a href="/article/{{ a.id }}">{{ a.title }}</a> ({{ a.views|number_format }} views)</li>
{% else %}
<li>記事がありません</li>
{% endfor %}
</ul>
{% endif %}
{# 自動エスケープ #}
{{ user.name }} {# <script> はエスケープされる #}
{{ user.bio|raw }} {# raw = エスケープしない(信頼できる時のみ) #}
{{ price|number_format(0, ',', '.') }}
{% endblock %}</code></pre>
<p>PHP からの呼び出し:</p>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-php"><?php
require __DIR__ . '/vendor/autoload.php';
$loader = new \Twig\Loader\FilesystemLoader(__DIR__ . '/templates');
$twig = new \Twig\Environment($loader, [
'cache' => __DIR__ . '/cache',
'auto_reload' => true, // 開発時は変更検知
'autoescape' => 'html',
]);
echo $twig->render('page.twig', [
'pageTitle' => '記事一覧',
'articles' => [
['id' => 1, 'title' => 'PHP 入門', 'views' => 12345],
['id' => 2, 'title' => 'Laravel', 'views' => 9876],
],
'user' => ['name' => 'taro', 'bio' => '<b>Hello</b>'],
]);</code></pre>
<h2>Blade の基本(Laravel)</h2>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-blade">{{-- layouts/app.blade.php --}}
<!DOCTYPE html>
<html>
<head>
<title>@yield('title', 'デフォルト')</title>
</head>
<body>
@include('partials.header')
@yield('content')
@stack('scripts')
</body>
</html>
{{-- pages/articles.blade.php --}}
@extends('layouts.app')
@section('title', '記事一覧')
@section('content')
<h1>{{ $pageTitle }}</h1>
@forelse ($articles as $a)
<div>
<a href="/article/{{ $a->id }}">{{ $a->title }}</a>
({{ number_format($a->views) }} views)
</div>
@empty
<p>記事がありません</p>
@endforelse
{{ $user->name }} {{-- 自動エスケープ --}}
{!! $user->bio !!} {{-- エスケープ無効(信頼できる時のみ) --}}
@endsection
@push('scripts')
<script src="/js/articles.js"></script>
@endpush</code></pre>
<p>コンポーネント(Blade の現代的な機能):</p>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-blade">{{-- resources/views/components/alert.blade.php --}}
@props(['type' => 'info'])
<div class="alert alert-{{ $type }}">
{{ $slot }}
</div>
{{-- 使用 --}}
<x-alert type="success">
保存しました!
</x-alert></code></pre>
<h2>Smarty / Plates / Mustache の例</h2>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-smarty">{* Smarty 4 *}
{extends file="layout.tpl"}
{block name="content"}
<h1>{$pageTitle|escape}</h1>
{foreach $articles as $a}
<li>{$a.title|escape}</li>
{/foreach}
{/block}</code></pre><pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-php"><?php
// Plates — 素の PHP に近い
// templates/page.php
$this->layout('layout', ['title' => $pageTitle]);
?>
<h1><?= $this->e($pageTitle) ?></h1>
<?php foreach ($articles as $a): ?>
<li><?= $this->e($a['title']) ?></li>
<?php endforeach; ?></code></pre><pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-mustache">{{! Mustache - ロジックレス }}
<h1>{{pageTitle}}</h1>
<ul>
{{#articles}}
<li>{{title}} ({{views}} views)</li>
{{/articles}}
{{^articles}}
<li>記事がありません</li>
{{/articles}}
</ul></code></pre>
<h2>選び方の指針</h2>
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%">
<thead><tr style="background:#e3f2fd"><th>状況</th><th>推奨</th></tr></thead>
<tbody>
<tr><td>Laravel を使う</td><td>Blade(標準・統合済)</td></tr>
<tr><td>Symfony を使う</td><td>Twig(標準・統合済)</td></tr>
<tr><td>フレームワーク非依存・新規</td><td>Twig(最も成熟)</td></tr>
<tr><td>軽量・素の PHP に近い構文がいい</td><td>Plates</td></tr>
<tr><td>JS / Ruby と同じテンプレートを共有</td><td>Mustache</td></tr>
<tr><td>既存資産が大量にある</td><td>そのまま継続(Smarty 等)</td></tr>
</tbody>
</table>
<h2>パフォーマンス: コンパイルキャッシュ</h2>
<p>主要エンジンは初回ロード時にテンプレートを<strong>純粋な PHP コードへコンパイル</strong>し、<code>cache/</code> ディレクトリに保存します。2 回目以降はキャッシュされた PHP を <code>require</code> するだけなので、<strong>素の PHP と遜色ない速度</strong>になります。</p>
<p>デプロイ時のキャッシュ操作:</p>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-bash"># Laravel Blade
php artisan view:clear
php artisan view:cache
# Twig はコンパイル済キャッシュをクリア(手動)
rm -rf var/cache/prod/twig
# Smarty
$smarty->clearCompiledTemplate();</code></pre>
<h2>FAQ</h2>
<p><strong>Q: 素の PHP では駄目?</strong><br />
A: 小さなページなら可。ただし <strong>htmlspecialchars()</strong> を毎回書くのは現実的に漏れる → XSS の温床。テンプレートエンジンは自動エスケープが最大の利点。</p>
<p><strong>Q: Twig と Blade はどちらが速い?</strong><br />
A: 大きな差はなし。両方とも初回コンパイル後はキャッシュされた PHP として動く。<strong>OPcache</strong> 有効化のほうが影響大。</p>
<p><strong>Q: テンプレートに DB クエリを書いていい?</strong><br />
A: 非推奨。表示ロジックとデータ取得は分離(MVC)。テンプレート内では <strong>渡された変数を表示するだけ</strong>に留めるとメンテしやすい。</p>