WEB

WordPressで「Blade」テンプレートエンジンを使おう!

WordPressで「Blade」テンプレートエンジンを使おう!

こんにちは、王です。
僕は以前から「なぜWordPressには、デフォルトのテンプレートエンジンがないのだろう?」と不思議で仕方ありませんでした。PHPのコードをHTMLの中にごちゃ混ぜにするのは、どうも気持ちが悪いように感じてしまいます。

いい感じのテンプレートエンジン探しの旅で、たどり着いたのが「Blade」。
知っている方もいらっしゃると思いますが、最近人気のPHPフレームワークである「Laravel」に、標準搭載されているテンプレートエンジンです。
もちろん、本家のBladeはそのままではWordPressでは使えません。Mikael MattssonさんによるWordPressの移植版が出ていますので、下記ページからそちらを使わせていただいてます。

Bladeの記述法

記述が本当に簡単なので、わかりやすいです!
紹介ページのサンプルコードを拝借してご紹介します。

{{$foo}}

コンパイル後↓

<?php echo $foo ?>

if()

@if(has_post_thumbnail())
    {{the_post_thumbnail() }}
@else 
    <img src="{{bloginfo( 'template_url' )}}/images/thumbnail-default.jpg" />
@endif

コンパイル後↓

<?php if(has_post_thumbnail()) : ?>
    <?php the_post_thumbnail() ?>
<?php else: ?>
    <img src="<?php bloginfo( 'template_url' ) ?>/images/thumbnail-default.jpg" />
<?php endif; ?>

ループ

@wpposts
    <a href="{{the_permalink()}}">{{the_title()}}</a><br>
@wpempty
    <p>404</p>
@wpend

コンパイル後↓

<?php if ( have_posts() ) : while ( have_posts() ) : the_post(); ?>
    <a href="<?php the_permalink() ?>"><?php the_title() ?></a><br>
<?php endwhile; else: ?>
    <p>404</p>
<?php endif; ?>

wordpress query

<ul>
@wpquery(array('post_type' => 'post'))
    <li><a href="{{the_permalink()}}">{{the_title()}}</a></li>
@wpempty
    <li>{{ __('Sorry, no posts matched your criteria.') }}</li>
@wpend
</ul>

コンパイル後↓

<ul>
<?php $query = new WP_Query( array('post_type' => 'post') ); ?>
<?php if ( $query->have_posts() ) : ?>
    <?php while ( $query->have_posts() ) : $query->the_post(); ?>
        <li><a href="<?php the_permalink() ?>"> <?php the_title() ?> </a></li>
    <?php endwhile; ?>
<?php else : ?>
    <li><?php _e('Sorry, no posts matched your criteria.') ?></li>
<?php endif; wp_reset_postdata(); ?>
</ul>

Advanced Custom Fields

<ul>
    @acfrepeater('images')
        <li>{{ get_sub_field( 'image' ) }}</li>
    @acfend
</ul>

コンパイル後↓

<ul>
    <?php if( get_field( 'images' ) ): ?>
        <?php while( has_sub_field( 'images' ) ): ?>
            <li><img src="<?php the_sub_field( 'image' ) ?>" /></li>
        <?php endwhile; ?>
    <?php endif; ?>
</ul>

他のテンプレートをインクルードする

@include('header')

layout & section

Bladeには「レイアウト」と「セクション」という概念があります。これだけちょっと説明が必要になりそうです。

「レイアウト」とは、使い回しが効くテンプレートファイルのようなもので、「セクション」とはレイアウトの中の要素要素のことです。実際に例を見てみましょう。

page.phpというファイルがあるとしましょう。
まず、@layout()を使って、利用したいレイアウトを指定します。「テーマディレクトリ/layout/master.php」というファイルを指定するとしたら「@layout('layout.master')」というふうに表現します。絶対パスと拡張子は不要です。

つづいて、@section(●●) ▲▲ @endsectionでセクションにしたい区域を指定します。「●●」はセクション名で、「▲▲」はセクションの内容になります。

page.php:

@layout('layout.master')

@section('content')
    <p>Lorem ipsum</p>
@endsection

レイアウトファイルmaster.phpで、@yield(●●)と記述すれば、そこに「▲▲」が挿入されます。

master.php:

<html>
    <div class="content">
        @yield('content')
    </div>
</html>

最終的にはこのようにコンパイルされます↓

<html>
    <div class="content">
        <p>Lorem ipsum</p>
    </div>
</html>

その他

よく使うのはこれくらいでしょうか。それ以外のタグは、Bladeの公式ドキュメントをご参照ください。

テンプレートエンジンの使い方と仕組み

テンプレートエンジンの使い方と仕組みをご紹介します。

使い方

使い方はいたって簡単!
「Blade」プラグインをオンにすれば、通常のWordPressのテンプレートファイル(page.phpとか、single.phpとか……)で先ほど紹介したキーワードが使えるようになります。とりあえず、試してみてください!

仕組み

仕組みも簡単です。
WordPressがロードしようとしているテンプレートファイルを、こっそりBladeがコンパイルしたファイルにすり替えているだけです。コンパイルされたファイルは「wp-content/plugins/blade/storage」に置かれています。

Bladeのキーワードと通常のPHPコードは混在できるため、テンプレートエンジンを使うために特別な手順は何一つ必要がないです。

PhpStormでBladeテンプレートのシンタックスハイライトを使いたい

PhpStormは、もともとBladeをサポートしています。ただし、拡張子が「.blade」の場合のみ。「.php」のファイルには適用できない模様です……。

でも使いたい!! どうしよう……?
かなり強引なやり方ですが、各テンプレートファイルに対応する「.blade」版のファイルを作って、こっちですべて作業します。「*.blade」ファイルを監視して、ファイルが保存されたら「.php」として書き出せばいいのです。手動ではさすがに手間が掛かるので、Gulpにやってもらうことにしました。

以下、`gulpfile.js`の内容になります。`default`タスクを実行すれば監視してくれます。もっといい解決法が出てくるまで、これで我慢しましょう!

(function() {
  var BLADE_TEMPLATES, File, duplicate, fs, globby, gulp, is_expired, path;

  fs = require('fs');

  path = require('path');

  gulp = require('gulp');

  globby = require('globby');

  BLADE_TEMPLATES = 'wp-content/themes/**/*.blade.php';

  File = function(file_path) {
    this.name = path.basename(file_path, '.blade.php');
    this.dir = path.dirname(file_path);
  };

  is_expired = function(template, compiled) {
    var compiled_mtime, template_mtime;
    if (fs.existsSync(compiled)) {
      template_mtime = fs.statSync(template).mtime.getTime();
      compiled_mtime = fs.statSync(compiled).mtime.getTime();
      if (template_mtime > compiled_mtime) {
        return true;
      } else {
        return false;
      }
    }
    return true;
  };

  duplicate = function(file_path) {
    var file, new_file_path, reader, writer;
    file = new File(file_path);
    new_file_path = "" + file.dir + "/" + file.name + ".php";
    if (!is_expired(file_path, new_file_path)) {
      return;
    }
    reader = fs.createReadStream(file_path, {
      encoding: 'utf8'
    });
    writer = fs.createWriteStream(new_file_path);
    reader.pipe(writer);
    return writer.on('finish', function() {
      return console.log("Duplicated: " + new_file_path);
    });
  };

  gulp.task('blade', function() {
    return gulp.watch(BLADE_TEMPLATES, function(e) {
      var file_path;
      file_path = e.path;
      return duplicate(file_path);
    });
  });

  gulp.task('compile_all_blade_template', function() {
    return globby(BLADE_TEMPLATES, function(err, paths) {
      return paths.forEach(function(file_path) {
        return duplicate(file_path);
      });
    });
  });

  gulp.task('default', ['compile_all_blade_template', 'blade']);

}).call(this);

ViewとModelを分けてみる

せっかくテンプレートエンジンが使えたので、それをフルに活かすため、簡単にViewとModelを分けてみました。ご参考までに。

structure

すごく単純化したディレクトリ構成はこんな感じです。とりあえず、それぞれのファイルの中身を見てみましょう。

functions.php

functions.phpでは、同階層にあるfunctionsフォルダの中の全てのPHPファイルを読み込むようにしています。無駄にたくさんの関数をfunctions.phpに詰め込むと、整理整頓が難しいですから。

<?php
/*
 * functionsフォルダにあるファイルをすべて読み込む
*/

foreach ( glob( TEMPLATEPATH . "/functions/*.php" ) as $file ) {
  require_once $file;
}

functions/route.php

<?php

require_once dirname(dirname( __FILE__ )).'/app/Route.php';
use \App\Route;

Route::init();

app/Route.php

$viewsで「ページ一覧」を記述しておき、is_page()のほうでその判定式を書いています。

<?php namespace App;
require_once 'Model.php';
use Closure;

class Route {
  static public $views = array(
    'home',
    'page',
  );

  /**
   * $viewsにあった「ページ」にアクセスしたら、必要なデータを取得するようにする
   */
  static public function init() {
    foreach ( static::$views as $view ) {
      static::listen( $view, function ( $wp ) use ( $view ) {
        global $d;
        $d = new Model( $view );
      } );
    }
  }

  /**
   * 該当のページと該当のコールバックを関連付ける
   *
   * @param $view
   * @param callable $callback
   *
   * @return void
   */
  static public function listen( $view, Closure $callback ) {
    add_action( 'wp', function ( $wp ) use ( $view, $callback ) {
      if ( in_array( $view, static::$views ) AND static::is_page( $view ) ) {
        $callback( $wp );
      }
    } );
  }

  /**
   * 該当のページかどうかを判断する
   *
   * @param string $view
   *
   * @return bool
   */
  static public function is_page( $view ) {
    if ( is_admin() ) {
      return false;
    }
    switch ( $view ) {
      case 'home':
        return is_home();
      case 'page':
        return is_page();
    }

    return false;
  }
}

BaseModel.php & Model.php

BaseModel.php

<?php
namespace App;

abstract class BaseModel {
  public $posts = array();
  public $extra, $common;

  function __construct( $view ) {
    $this->extra  = (object) array();
    $this->common = (object) array();
    $method       = "set_{$view}";
    $this->set_common();
    if ( method_exists( $this, $method ) ) {
      $this->$method();
    }
  }

  /**
   * 共通データの設定を行う
   */
  abstract protected function set_common();

  /**
   * ヘルパーメソッド。渡されたクロージャをThe-Loopの中で呼ぶ。
   *
   * @param callable $callback         実引数にidを渡す
   * @param \WP_Query $wp_query_object WP_Queryのインスタンスを受け付ける
   */
  protected function each( \Closure $callback, \WP_Query $wp_query_object = null ) {
    // WP_Query Loop
    if ( $wp_query_object instanceof \WP_Query ) {
      if ( $wp_query_object->have_posts() ) {
        while ( $wp_query_object->have_posts() ) {
          $wp_query_object->the_post();
          $callback( get_the_ID() );
        }
      }
      wp_reset_postdata();

      return;
    }

    // Regular Loop
    if ( have_posts() ) {
      while ( have_posts() ) {
        the_post();
        $callback( get_the_ID() );
      }
    }
  }
}

Model.php

<?php
namespace App;
require_once 'BaseModel.php';

class Model extends BaseModel {
  // index.phpにアクセスしてきたときにやりたいこと。
  protected function set_home() {
    $this->extra->foo = 'foo';
  }

  // 「固定ページ」にアクセスしてきたときにやりたいこと。
  protected function set_page() {
    $this->extra->foo = 'bar';
  }

  /**
   * 共通データの設定を行う
   */
  protected function set_common() {
    $this->each( function () {
      $post_obj = array(
        'title'     => get_the_title(),
        'permalink' => get_the_permalink()
      );

      $this->posts[] = (object) $post_obj;
    } );
  }
}

各ページで行いたいことをここに記述する。メソッド名さえ規則に合っていれば、自動的に呼ばれます。

Routeクラスで定義した「ページ」名に、`set_`を付け加えた文字列がメソッド名になっています。例えば、「home」ページであれば、「set_home」メソッドが呼ばれます。

layout/master.php

レイアウトファイルです。
WordPressではget_header()get_footer()やらを使って、1つのファイルをバラバラに分割するという何とも不気味な文化を持っています。(header.phpでタグが閉じてないところとか……)

でも、Bladeでやると、1ページに全ての要素を記述することができます。挿入したいコードがあったら@yield()でできます。単純明快ですね! 具体的には下記をご覧ください。

index.php & page.php

index.php

@layout('layout.master')

@section('css')
<style>
  h1 {
    color: #002a80;
  }
</style>
@endsection

@section('js')
<script>alert('index.php')</script>
@endsection

@section('body')
<h1>{{ $d->extra->foo }}</h1>
<ul>
  @foreach($d->posts as $p)
  <li><a href="{{ $p->permalink }}">{{ $p->title }}</a></li>
  @endforeach
</ul>
@endsection

page.php

@layout('layout.master')

@section('body')
  @foreach($d->posts as $p)
    <h1>{{ $p->title }}</h1>
  @endforeach
@endsection

テンプレートのほうでは「$d」というグローバル変数(オブジェクト)から、必要なデータを出力すればいいのです。

それから、@sectionを使って、CSSとJavaScriptをmaster.phpheadの中に挿入しているのがわかるかと思います。各テンプレートで必要に応じてJSやCSSファイルを「いちいちページ判定せずに」挿入できるところが素晴らしいですね。

まとめ

いかがでしたでしょうか?
やり方は人それぞれだと思います。簡単な説明となりますが、以上が自分の実装例となります。

このやり方で試しに導入してみたら、だいぶメンテナンスがしやすくなったと感じました。最初は面倒に感じるかもしれませんが、後々が本当に楽です。特にスマホ対応の場合は同じコードをもう1回書かずに済むし、コードの見通しもしやすいです。

以上、WordPressのテンプレートエンジン「Blade」のおすすめでした。それでは!

 

【王の挑戦!】

WordPressのWidget(ウィジェット)を自作する方法

Retina対応のスプライトを作成するときに便利なCompass用のmixinを作ってみた

console.logのラッパーを作成して業務を効率化する方法

「RxJS」初心者入門 – JavaScriptの非同期処理の常識を変えるライブラリ

この記事を書いた人

王
バックエンドエンジニア 2012年入社
LIGの王です。ウェブの全てを学ぶ為、中国は四川省より日本にやってきました。王という名に恥じぬよう、ウェブ業界のKINGとなるべく日々頑張っております。よろしくお願いいたします。