a-blog cms での Twig の書き方を体験してみよう!

このハンズオン記事では、Ver. 3.2 で追加されるTwigテンプレートを活用する方法を解説します。

TwigはPHPでのテンプレートエンジンとして広く使われており、シンプルかつ強力なテンプレート機能を提供します。 Twigの基本からa-blog cmsのテンプレートでの利用方法ついて実践的に学びましょう。


Twigとは?

Twigは、PHPでよく使用されるテンプレートエンジンで、読みやすく保守しやすいコードを可能にします。 また、テンプレートとロジックを分離できるため、フロントエンドとバックエンドの開発を分けやすいのが特徴です。

Twigが使用されているCMSは以下のようなものがあります。

  • Drupal
  • Craft CMS
  • EC-CUBE

幅広く利用されているため、Twigに関する記事や、触った経験がある人も多く、a-blog cms でカスタマイズするハードルが下がることが期待できます

a-blog cms で Twigテンプレートを利用可能にする

Ver. 3.2 で Twigテンプレートに対応しますが、従来のテンプレート記法も使用することができます。 Twigと従来のテンプレート記法のどちらかを選択するということではなく、従来のテンプレート記法とTwig記法を混ぜてテンプレートをかけるようになります。

Twig記法を有効にする

「管理画面 > テーマ」から、使用テーマの編集画面に入り、以下スクショのように「Twigテンプレート」を有効にすることで、Twig記法が利用することができるようになります。


テーマ設定

テーマ設定


もしくは、テーマ内の「template.yaml」を以下のように設定することでも可能です。

tpl_top: xxxxxxx
tpl_index: xxxxxxx
tpl_detail: xxxxxxx
tpl_404: xxxxxxx
tpl_admin: xxxxxxx
tpl_entry_edit: xxxxxxx
tpl_entry_add: xxxxxxx
tpl_login: xxxxxxx
tpl_twig: enabled # この行を追加しTwigを有効化

テスト用のテンプレートを作成してみましょう

試しながら進めることが出来るようにテスト用のテンプレートを用意しましょう。

ご利用のテーマを「テーマ設定」から Twigを有効にしていただいたあと、テーマに「test.html.twig」を用意します。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Twigテスト</title>
</head>
<body>
  <h1>Twigのテスト</h1>
</body>
</html>

以下URLでこのテンプレートを表示できます。

https://example.com/test.html

テンプレートの拡張子の付け方については、後ほど解説します。

変数の書き方

従来の記法

従来の記法では、変数を中括弧({変数})で囲むことで出力できます。

<p>{name}</p>

Twigの記法

Twigでは、変数をダブル中括弧({{ 変数 }})で囲むことで出力できます。(Twig標準の仕様に従います)

<p>{{ name }}</p>

グローバル変数の書き方

従来の記法

従来の記法では、変数を%中括弧(%{変数})で囲むことで出力できます。

<h1>%{BLOG_NAME}</h1>

Twigの記法

Twigでは、グローバル変数と変数の記法に区別はありません。ダブル中括弧({{ 変数 }})で囲むことで出力できます。Twig標準の仕様に従います。

<h1>{{ BLOG_NAME }}</h1>

従来の記法では、グローバル変数に校正オプションを適応できませんでしたが、Twigでは変数と同じ記法になるので、グローバル変数に校正オプションも同様に適応できるようになります。

変数を定義する

Twig内で変数を定義することができます。変数定義には「setタグ」を使います。例えば、計算や文字列の組み立てなどに使用できます。 従来の記法では、SetTemplateやSetRenderedが近いですが、変数として利用できる点で違った機能になり、従来テンプレートには同様の機能はありません。(Twig標準の仕様に従います)

Twigの記法

{% set greeting = "こんにちは" %}
{% set name = "山田" %}

<p>{{ greeting }}, {{ name }}さん!</p>

文字列の連結

Twigでの「~」は、文字列を結合するために使用される演算子です。PHPでの「.」やJavaScriptでの「+」に相当し、文字列と他の変数を簡単に結合できます。

{% set title = "今日の天気" %}
{% set weather = "晴れ" %}

<p>{{ title }} は {{ weather }}です。</p>
<p>{{ title ~ "は" ~ weather ~ "です。" }}</p> <!-- 連結して出力 -->

校正オプション(フィルター)の書き方

従来の記法

従来の記法では、変数の後ろに大括弧({変数}[校正オプション1|校正オプション2])を追加して校正オプションを指定します。複数指定する場合は、パイプで繋ぎます。

<p>{content}[nl2br|safe_html]</p>

* safe_htmlは、Ver. 3.2 で追加される校正オプションです。危険なタグ以外の安全なHTMLを出力します。

Twigの記法

Twigの記法では、変数の中にパイプ(|)で区切りフィルターを指定します({{ 変数|フィルター1|フィルター2 }})(Twig標準の仕様に従います)

{% set content = '今日の天気は\n晴れです' %}

<p>{{ content|nl2br|safe_html }}</p>

使用できる校正オプション(フィルター)の種類

従来のa-blog cmsの校正オプションに加え、Twig標準のフィルターも全く同じ記法で指定することができます。

{% set sizes = [34, 36, 38, 40, 42] %}

{{ sizes|filter(v => v > 38)|join(', ') }}
{# output 40, 42 #}

引数に変数を指定

従来の校正オプションでは、一部校正オプションのみ引数に変数を利用できましたが、Twig記法だと全てのフィルターに変数が利用できます

{% set content = '文書を途中で切ります' %}
{% set length = 4 %}
{% set ellipsis = '...' %}

<p>
  {{ content|mb_trim(length, ellipsis) }}
</p>

フィルターを使用したデフォルト値

変数の値がなかった場合に、デフォルト値を出力することができます。

<p>{{ nickname | default("ゲスト") }}</p>

twigでの「raw」校正オプション(フィルター)の仕様が変更になります。

既存テンプレート

<!-- 危険なタグはエスケープされ、安全なHTMLは出力されます -->
{hoge}[raw]

<!-- scriptを含めエスケープを無効にして、値をそのまま出力します。使用箇所には注意が必要です。 -->
{hoge}[raw|allow_dangerous_tag]

Twigテンプレート

<!-- 危険なタグはエスケープされ、安全なHTMLは出力されます -->
{{ hoge|safe_html }}

<!-- scriptを含めエスケープを無効にして、値をそのまま出力します。使用箇所には注意が必要です。 -->
{{ hoge|raw }}

if文の書き方

従来の記法

従来の記法では、専用のコメントでif文を記述します。

<!-- BEGIN_IF [%{SESSION_USER_ID}/nem] -->
<p>
  ログイン中です
</p>
<!-- END_IF -->

Twigの記法

Twigでは「ifタグ」を使用して記述します。(Twig標準の仕様に従います)

{% if SESSION_USER_AUTH %}
  <p>
    ログイン中です
  </p>
{% endif %}

条件式の記述例

比較演算子

  • ==(等しい)
  • !=(等しくない)
  • >(大きい)
  • <(小さい)
  • >=(以上)
  • <=(以下)
{% set age = 18 %}

{% if age >= 18 %}
  <p>成人です。</p>
{% else %}
  <p>未成年です</p>
{% endif %}

論理演算子

  • and(かつ)
  • or(または)
  • not(否定)
{% set isLogin = true %}
{% set isAdmin = true %}

{% if isLogin and isAdmin %}
    <p>ログイン済みの管理者です。</p>
{% endif %}

{% if not isLogin %}
    <p>ログインしてください。</p>
{% endif %}

is 演算子

  • empty: 空かどうか
  • defined: 定義されているかどうか
  • null: nullであるかどうか
  • even/odd: 偶数か奇数か
  • divisible by: 指定した数で割り切れるか
{% set name = '山田' %}
{% set list = [] %}
{% set number = 6 %}

{% if name is defined %}
    <p>名前: {{ name }}</p>
{% endif %}

{% if list is empty %}
    <p>リストは空です。</p>
{% endif %}

{% if number is even %}
<p>
  {{ number }}は偶数です。
</p>
{% endif %}

{% if number is divisible by(2) %}
  <p>{{ number }}は2で割り切れます。</p>
{% elseif number is divisible by(3) %}
  <p>{{ number }}は3で割り切れます。</p>
{% else %}
  <p>{{ number }}は2でも3でも割り切れません。</p>
{% endif %}

in 演算子

{% set colors = ["red", "blue", "green"] %}

{% if "blue" in colors %}
    <p>青色が含まれています。</p>
{% endif %}

他のプログラミング言語と同様に、括弧を使って条件式の優先度を制御できます。

モジュールの呼び出し方

従来の記法

従来の記法では、専用のコメント文で囲んだ範囲にモジュールの変数を展開します。

<!-- BEGIN_MODULE Entry_Summary id="xxxxx" ctx="bid/1" -->
<p>{indexBlogName}<p>
<!-- END_MODULE Entry_Summary -->

Twigの記法

Twigを拡張し、a-blog cms独自の関数を追加しています。関数で呼び出したモジュールのデータを変数に格納して使用します。

{% set hoge = module('V2_Entry_Summary', 'xxxxx', 'bid/1') %}

<p>{{ hoge.indexBlogName }}</p>

* 「モジュールID」と「URLコンテキスト」は、従来モジュール同様に省略可能です。

{% set hoge = module('V2_Entry_Summary') %}

Twig記法では従来のモジュールは呼び出せないのでご注意ください。Twigで呼び出せるモジュールはモジュール名の最初が「V2」から始まるモジュールのみになります。

Ver. 3.2.0-alpha 版では一部モジュールのみV2モジュールに対応しています。現状Twigで使用できるモジュールは以下となります。

  • V2_Entry_Summary
  • V2_Entry_Body
  • V2_Entry_TagRelational
  • V2_Category_Tree
  • V2_Media_Banner
  • V2_Blog_Field
  • V2_Module_Field
  • V2_Links
  • V2_Tag_Filter
  • V2_Tag_Cloud
  • V2_Topicpath
  • V2_User_Profile
  • V2_Ogp

名前が似ていても従来のモジュールとは出力される変数や構造は違いますのでご注意ください。

タッチモジュールの呼び出し方

従来の記法

従来の記法では、専用のコメント文で囲んだ範囲の表示・非表示を制御します。

<!-- BEGIN_MODULE Touch_Login -->
<p>ログイン中です</p>
<!-- END_MODULE Touch_Login -->

Twigの記法

Twigを拡張し、a-blog cms独自の関数を追加しています。関数で呼び出すことで真偽値が返却されるので、if記法と併用して出し分けます

{% if touch('Touch_Login') %}
  <p>
    ログイン中です
  </p>
{% endif %}

touch関数は、module関数と違い既存のすべてのタッチモジュールを呼び出せます

表示・非表示を制御しているのは、単なる「ifタグ」なので、条件式をtouch関数だけではなく組み合わせて利用することもできます。

{% if touch('Touch_Login') and IS_ADMIN %}
  <p>ログイン中 かつ 管理ページの場合<p>
{% endif %}

ループ処理の書き方

従来の記法

従来の記法では、モジュールが出力するループブロックで囲んだ範囲がループして出力されます。

<!-- BEGIN_MODULE Entry_Summary -->
<ul>
  <!-- BEGIN entry:loop -->
  <li>{title}</li>
  <!-- END entry:loop -->
</ul>
<!-- END_MODULE Entry_Summary -->

Twigの記法

Twigでのループの書き方はとてもシンプルです。「forタグ」を使って、配列やリストを簡単にループ処理できます。基本的な構文は以下の通りです。(Twig標準の仕様に従います)

{% set fruits = ['Apple', 'Banana', 'Cherry'] %}

<ul>
    {% for fruit in fruits %}
        <li>{{ fruit }}</li>
    {% endfor %}
</ul>

モジュールで取得したデータをループする例

{% set entrySummary = module('V2_Entry_Summary') %}

<ul>
  {% for entry in entrySummary.entries %}
    <li>{{ entry.title }}</li>
  {% endfor %}
</ul>

インデックスの取得

「loop」という特殊な変数を使用して、ループのインデックスやその他の情報を取得できます。loop.indexで1から始まるインデックスを取得したり、loop.index0で0から始まるインデックスを取得できます。

<ul>
  {% for entry in entrySummary.entries %}
    <li>{{ loop.index }}: {{ entry.title }}</li>
  {% endfor %}
</ul>

その他のloopオプション

  • loop.first:ループの最初の要素の場合にtrueを返す
  • loop.last:ループの最後の要素の場合にtrueを返す
  • loop.revindex:残りの要素数(1からカウント)
  • loop.revindex0:残りの要素数(0からカウント)

「loop.first」や「loop.last」を使用することで、従来の <!-- BEGIN glue --> の代替ができます。

{% for entry in entrySummary.entries %}
  {{ entry.title }}
  {% if not loop.last %} | {% endif %}
{% endfor %}

インクルードの書き方

Twigでのインクルードの書き方について解決します。(Twig標準の仕様に従います)

基本的なインクルード

include関数を使ってテンプレートをインクルードします。

{{ include('/path/to/parts.twig') }}

変数を渡す

インクルードするテンプレートに変数を渡したい場合は、第2引数として変数を渡します

{{ include('/path/to/parts.twig', { title: 'タイトルです' }) }}

このように書くことで、/path/to/parts.twig 内でtitle変数が使えるようになります。

ファイルが存在しない場合のエラー回避

従来のインクルード文だと、インクルードするファイルがない場合でもエラーは発生しませんでしたが、Twigのインクルードはファイルが存在しない場合エラーが出力されます。

インクルードするファイルが存在しない場合にエラーを回避するには、第3引数にtrueを渡してignore_missingオプションを有効にします。

{{ include(/path/to/parts.twig', {}, true) }}
or
{{ include('/path/to/parts.twig', {}, ignore_missing = true) }}

インクルード文の中にはグローバル変数以外にも全ての変数が利用できます。

従来のインクルードだと、インクルードパス内に利用できる変数はグローバル変数かつ特定のものに制限されていましたが、Twigのインクルードはすべての変数をインクルードパスに含めることが可能です。

{% set filename = 'hoge' %}

{{ include('/include/' ~ filename ~ '.twig', ignore_missing = true) }}

テンプレート継承の書き方

Twigでも従来テンプレートと同じようにテンプレートの継承機能が利用できます。Twig標準の仕様に従っているため、ここでは詳しく説明しません。詳しくはTwigのドキュメントを参照ください。

Twigの継承機能のドキュメント

Twigのテンプレート継承の例

継承元テンプレート

<!DOCTYPE html>
<html>
  <head>
    {% block headMeta %}
      {{ include('/include/head/meta.twig') }}
    {% endblock %}
    {% block headLink %}
      {{ include('/include/head/link.twig') }}
    {% endblock %}
    {% block headJs %}
      {{ include('/include/head/js.twig') }}
    {% endblock %}
  </head>
  <body>
    <header>
      <h1>ヘッダー</h1>
    </header>
    <main>
      {% block main %}
        <p>継承してメインコンテンツを設定してください</p>
      {% endblock %}
    </main>
    <footer>
      {% block footer %}
        <p>Site Footer</p>
      {% endblock %}
    </footer>
  </body>
</html>

継承したテンプレート

{% extends 'parent_template.twig' %}

{% block headLink %}
  {{ parent() }}
  <link rel="stylesheet" href="/include/edit/custom.css" />
{% endblock %}

{% block main %}
  <p>
    メインコンテンツを上書き
  </p>
{% endblock %}

Twigのブロック名にはいくつかの命名規則があります

  • 英字(アルファベット)と数字、アンダースコア(_)のみが使用可能
  • 英字から始めなければならない

有効なブロック名の例

  • headJs
  • head_js
  • headJs123

無効なブロック名の例

  • head-js
  • 123headJs

テンプレートの解決順

Twigが有効なテーマの場合、以下の順序でテンプレートが解決されます。

  1. インクルードなどを含め、Twigによる記述が解決される
  2. Twigが解決されたあとのテンプレートを従来のテンプレート記法部分が解決される

順番に解決される特性上、テンプレートが解決されないパターンがありますので、いくつかパターンを紹介します。

テンプレートが解決されないパターン

  • 従来の @include で読み込んだテンプレートにあるTwig記法は解決されない
  • 従来の @extends で継承したテンプレート以下にあるTwig記法は解決されない
  • 従来の @extends で継承されているテンプレートは、Twig記法でインクルード出来ない

Twigテンプレートの拡張子

テンプレートの拡張子について解説します。

拡張子は「twig」がおすすめ

特に拡張子に縛りはありません。そのため、拡張子が .html のテンプレートにTwigの記法で記述しても正しく動作します。

ただし、Twigで記述する場合は拡張子を .twig にすることをおすすめします。理由は、エディタ側が編集中のテンプレートをTwigとして認識することで、ハイライト表示やTwigフォーマットへの対応など、エディタの便利な機能を活用できるからです。

URLでテンプレートを指定する場合

動的テンプレートやインクルードファイルでは、テンプレートファイル名をURLで指定することがないため、特に意識する必要はありません。 しかし、URLでテンプレートを直接指定する場合は、少し工夫が必要です。

たとえば、検索結果ページを https://example.com/search.html で表示するケースがあると思います。 このとき、テンプレートファイル名を search.twig にしてしまうと、URLが https://example.com/search.twig になってしまいます。

これを避けるために、テンプレートファイル名を search.html.twig にします。

URLで指定されたファイル名に対して、自動的に .twig 拡張子のテンプレートも検索する機能があるため、URLとファイル名が異なっていても、search.html.twig のテンプレートが正しく表示される仕組みになっています。

モジュール変数の確認方法

現状のα版パッケージでは、Twig対応のV2モジュールの変数表やスニペットが用意されていないため、どのような変数があるか分かりません。そこで以下方法でモジュールの出力内容を確認できるようにします。

ご利用テーマの直下に「module.json.twig」を用意します。

{{ module('V2_Entry_Summary')|jsonPrint|raw }}

表示したいモジュール名を指定します。「jsonPrint」フィルターでJSONに変換させ、モジュールが持っているデータを見えやすく表示します。

あとは、以下URLでモジュールの内容を表示できます。

https://example.com/module.json

ブラウザでモジュールデータをJSONで表示

ブラウザでモジュールデータをJSONで表示


オブジェクト・配列へのアクセス方法

慣れていないと、モジュールなどで取得した複雑なデータへのアクセスが難しいので、ここで学習と練習をしましょう。

オブジェクトへのアクセス

オブジェクトとは、キーと値のペアを持つデータの集合になります。

{
  key1: 'key1の値',
  key2: 'key2の値'
}

オブジェクトの値にアクセスするには、ドット記法またはブラケット記法を使用します。

  • ドット記法:object.property
  • ブラケット記法:object["property"]
{% set user = { name: '山田', age: 18 } %}

{{ user.name }}  {# 山田 #}
{{ user["age"] }}  {# 18 #}

配列へのアクセス

配列は、データを順番に並べて管理するためのデータ構造です。

["りんご", "バナナ", "みかん"]

配列の要素には、インデックス番号でアクセスします。Twigでは、インデックスは0から始まります。

{% set fruits = ['りんご', 'バナナ', 'みかん'] %}

{{ fruits[0] }}  {# りんご #}
{{ fruits[2] }}  {# みかん #}

また配列はループで回すことができます。

{% set fruits = ['りんご', 'バナナ', 'みかん'] %}

{% for fruit in fruits %}
  {{ fruit }}
{% endfor %}

練習問題

以下コードをテンプレートにはり、以下問題に挑戦してみてください。

{% set data = {
  title: 'テストデータ',
  fruits: ['りんご', 'バナナ', 'みかん'],
  users: [
    { name: '山田', age: '18', address: { city: '東京都', postalCode: '100-0005' } },
    { name: '佐藤', age: '30', address: { city: '大阪府', postalCode: '530-0001' } },
    { name: '鈴木', age: '45', address: { city: '愛知県', postalCode: '460-0002' } }
  ]
} %}

問題例 2番目「fruits」を表示してください。

{{ data.fruits[1] }}  {# バナナ #}

問題1 最初の果物の名前を表示してください。(「りんご」が出力されればOK)

問題2 2番目のユーザーの名前を表示してください。(「佐藤」が出力されればOK)

問題3 最後のユーザーの住所(city)を表示してください。(「愛知県」が出力されればOK)

問題4 for文を使って果物を全て表示してください。(「りんご、バナナ、みかん」が出力されればOK)

問題5 for文を使って各ユーザーの名前と住所を以下の形式で表示してください。

山田 - 東京都
佐藤 - 大阪府
鈴木 - 愛知県

その他のTwig機能について

a-blog cms での Twig の基本的な使い方について学んできましたが、他にも便利なTwigの機能がありますので、ぜひ目を通してみてください。

Twigのドキュメント

たとえば「V2_Category_Tree」モジュールはデータが以下のように階層構造になっており、 階層すべてを表示しようと思うと、階層分の「for文」をテンプレートに書く必要があります。

V2_Category_Treeのデータ構造例 (一部データを省略しています)

{
  categories:
    [
      {
        id: 1,
        name: "お知らせ",
      },
      {
        id: 2,
        name: "製品",
        children:
          [
            {
              id: 3,
              name: "家庭用製品",
            },
            {
              id: 4,
              name: "業務用製品",
              children:
                [
                  {
                    id: 6,
                    name: "業務用製品のお知らせ",
                  },
                ],
            },
          ],
      },
    ]
}

for文のみを使用したテンプレート例

{% set tree = module('V2_Category_Tree') %}

<ul>
  {% for category in tree.categories %}
    <li>
      {{ category.name }}
      {% if category.children is not empty %}
        <ul>
          {% for child in category.children %}
            <li>
              {{ child.name }}
              {% if child.children is not empty %}
                <ul>
                  {% for child2 in child.children %}
                    <li>{{ child2.name }}</li>
                  {% endfor %}
                </ul>
              {% endif %}
            </li>
          {% endfor %}
        </ul>
      {% endif %}
    </li>
  {% endfor %}
</ul>

これをTwigの「macro(マクロ)機能」を使うことで、シンプルに記述することができます。

macroを使用したテンプレート例

{% set tree = module('V2_Category_Tree') %}

{% macro renderCategories(categories) %}
  <ul>
    {% for category in categories %}
      <li>
        {{ category.name }}
        {% if category.children is not empty %}
          {{ _self.renderCategories(category.children) }}
        {% endif %}
      </li>
    {% endfor %}
  </ul>
{% endmacro %}

{{ _self.renderCategories(tree.categories) }}

色々触ってみましょう!

以上でTwig機能の解説は終わりになります。お疲れ様でした!

残り時間は、この記事で学んだことを実際に色々と試してみてください。既存テーマを一部Twigで書き直してみるとより理解が進むと思います。

配布しているα版パッケージのテーマに「twig@blog」という「blog」テーマをTwigで書き直したものも同梱されていますので、ぜひ参考ください。

同じタグ付けがされている記事