はじめに
近年、BEAR.SundayのプロジェクトではテンプレートエンジンにQiqを採用しています。Qiqには2つの重要な特徴があります。
Qiqの特徴
1. 明示的なエスケープ
一つ目は、自動エスケープではなく明示的なエスケープを要求する点です:
<a href="{{a $link}}"> // HTML属性として安全にエスケープ
{{h $var }} // HTMLとして安全にエスケープ
</a>
一見すると、自動エスケープの方が安全で楽に見えるかもしれません。しかし、明示的なエスケープには重要な利点があります:
- コンテキストの明確化:HTMLなのか、URLなのか、出力の文脈が一目瞭然
- 適切なエスケープの強制:異なるコンテキストに応じた適切なエスケープ方法の選択
- 意図の明示:どのような加工が行われているかがコードから明確
2. 独自の制御構文を持たない
もう一つの特徴は、独自の制御構文を持たず、PHPの構文をそのまま利用する点です:
// Twigの場合
{% if user.isAdmin %}
{{ user.name }}
{% endif %}
// Qiqの場合
{{ if ($user->isAdmin): }}
{{h $user->name }}
{{ endif }}
この選択は、「そもそもテンプレートエンジンにどこまでの制御構造が必要なのか?」という、より本質的な問いへとつながります。
テンプレートエンジンが抱えるよくある問題
Qiqのように優れたテンプレートエンジンであっても、テンプレートエンジンにはいくつかの課題があります。
1. コードの可視性
- IDEや外部ツールによる静的解析が及ばない
- コードの品質測定やリファクタリング支援が効かない
- 型の追跡が途切れる「ブラックボックス」となってしまう
- テンプレート内のロジックは単体テストが困難
2. 責務の境界の曖昧さ
- フロントエンド・バックエンドの境界に位置するため、双方の関心事が混在しやすい
- ビジネスロジックが紛れ込みやすい
- 様々な判断ロジックの「最後の砦」となってしまう
制約による解決
これらの問題に対する解決策は、一見すると困難に思えますが、実はシンプルです。
テンプレートエンジンを本来の責務に限定するという「制約」を設けることです。
具体的には、以下の3つの責務に限定します:
- レイアウトの継承
- (コンテンツ)ブロックの定義と配置
- 安全な出力
テンプレートから制御構造を取り除く
テンプレート内のロジックで特に複雑になりがちなのは、foreachループとその中での条件分岐です。ループが入れ子になったり、複数の条件分岐が組み合わさったりすることで、可読性と保守性が急速に低下してしまいます。
これらの問題に対しては、ジェネレータを使用することで効果的に対処できます。
具体例を見てみましょう。
まずは、ジェネレータを使わないケースです。
{{ foreach ($items as $item): }}
{{ if ($item->isAvailable()): }}
{{ if ($item->getStock() > 0): }}
{{ if ($item->isDiscounted()): }}
<li class="sale">{{h $item->name }} - {{h $item->getDiscountPrice() }}</li>
{{ else: }}
<li>{{h $item->name }} - {{h $item->price }}</li>
{{ endif }}
{{ endif }}
{{ endif }}
{{ endforeach }}
このように改善できます。
// プレゼンター側
class ProductPresenter
{
public function getDisplayItems(): Generator
{
foreach ($this->items as $item) {
if (!$item->isAvailable() || $item->getStock() <= 0) {
continue;
}
yield [
'name' => $item->getName(),
'price' => $item->isDiscounted()
? $item->getDiscountPrice()
: $item->getPrice(),
'classes' => $item->isDiscounted() ? 'sale' : '',
];
}
}
}
// テンプレート側:シンプルで宣言的
{{ foreach ($displayItems as $item): }}
<li {{a ['class' => $item['classes'] ]}}>
{{h $item['name'] }} - {{h $item['price'] }}
</li>
{{ endforeach }}
テンプレートから外部の値にアクセスしない
BEAR.Sundayでは、リソースの取得を機能の一つであるEmbedによって行います。
class Index extends ResourceObject
{
#[Embed(rel: 'todos', src: 'app://self/todos{?status}')]
public function onGet(string $status): static
{
return $this;
}
}
テンプレートから、そのリソースを参照できるのですが、これを行わないようにします。
// NG 参照
{{h $this->todos->title }}
// NG 別のテンプレートに渡す
{{ render ('/template/todos', ['todos' => $this->todos ]) }}
// OK 出力だけを行う
{{= $this->todos }}
リソース自身がテンプレートを持ち、その中で出力を行います。
{{h $this->todos->title }}
{{ foreach ($this->todos->entries as $entry): }}
{{h $entry->title }}
{{ endforeach }}
こうすることで、リソースそれぞれを文字列としてキャッシュすることが可能になります。
制約がもたらす恩恵
これらの制約を採用することで、以下のような恩恵が得られます。
1. テスタビリティの向上
- ビジネスロジックをテンプレートから分離することで、ロジックの単体テストが容易になります
- テンプレートは純粋に表示のみを担当するため、テストが単純化されます
2. キャッシュの最適化
- テンプレートが外部にアクセスしないため、出力結果を文字列として安全にキャッシュできます
3. メンテナンス性の向上
- 責務の境界が明確
- コードの意図が理解しやすい
- ビジネスロジックとプレゼンテーションの明確な分離
制約は福音
制約がなぜ必要なのかを理解することは難しいことですし、理解した上でそれを守り続けることも難しいです。
「そこまでの制約は必要ない」「ここにこう書いてしまえば悩まずにすむ」といった声も聞かれるでしょう。
しかし制約は、将来の複雑さから私たちを守る「福音」なのです。
一方、制約を緩めることは、短期的な利便性と引き換えに、長期的な保守性と信頼性を損なう「凶報」となりかねないのです。
まとめ
本記事では、BEAR.SundayプロジェクトにおけるテンプレートエンジンとしてQiqを採用する理由と、守るべき制約について詳しく解説しました。
適切な制約は私たちを混乱から守り、本来の目的である「テンプレートで何を表現したいのか」という本質に集中することを助けてくれます。結果として、長期的に見ると、より理解しやすく、保守がしやすく、そして安全なコードが自然と書けるようになります。