こんにちは、王です。
僕は以前から「なぜWordPressには、デフォルトのテンプレートエンジンがないのだろう?」と不思議で仕方ありませんでした。PHPのコードをHTMLの中にごちゃ混ぜにするのは、どうも気持ちが悪いように感じてしまいます。
いい感じのテンプレートエンジン探しの旅で、たどり着いたのが「Blade」。
知っている方もいらっしゃると思いますが、最近人気のPHPフレームワークである「Laravel」に、標準搭載されているテンプレートエンジンです。
もちろん、本家のBladeはそのままではWordPressでは使えません。Mikael MattssonさんによるWordPressの移植版が出ていますので、下記ページからそちらを使わせていただいてます。
記述が本当に簡単なので、わかりやすいです!
紹介ページのサンプルコードを拝借してご紹介します。
{{$foo}}
コンパイル後↓
<?php echo $foo ?>
@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; ?>
<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>
<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をサポートしています。ただし、拡張子が「.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を分けてみました。ご参考までに。
すごく単純化したディレクトリ構成はこんな感じです。とりあえず、それぞれのファイルの中身を見てみましょう。
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.php
のhead
の中に挿入しているのがわかるかと思います。各テンプレートで必要に応じてJSやCSSファイルを「いちいちページ判定せずに」挿入できるところが素晴らしいですね。
いかがでしたでしょうか?
やり方は人それぞれだと思います。簡単な説明となりますが、以上が自分の実装例となります。
このやり方で試しに導入してみたら、だいぶメンテナンスがしやすくなったと感じました。最初は面倒に感じるかもしれませんが、後々が本当に楽です。特にスマホ対応の場合は同じコードをもう1回書かずに済むし、コードの見通しもしやすいです。
以上、WordPressのテンプレートエンジン「Blade」のおすすめでした。それでは!
【王の挑戦!】
※ WordPressのWidget(ウィジェット)を自作する方法
※ Retina対応のスプライトを作成するときに便利なCompass用のmixinを作ってみた