もろもろの事情からWordPressをカスタマイズしてサービスを作る(らなきゃいけない)プロジェクトが続いていて、現在2つ目のサービスを開発中。1つ目のサービスは勝手がわからぬままガシャガシャと力技で組み上げたので、functions.php
とかそれはもうひどいことになってたんだけど、今回はできるだけモダンに、スマートに、テスタブルに書きたいなと思ってプロジェクト開始前にけっこう準備をした。目標はテーマをMVCっぽく書けるようにして、テスタブルにすること。
テーマMVCの仕組み
オレオレ臭がプンプンするけど、とにかくエレメント毎にMとVとCを分けて書けるようにした。本当はファイル名やクラス名にプロジェクトのプレフィックスが入ってるんだけど、大人の事情もあるので消して掲載する。
ディレクトリ構成
テーマ内のディレクトリ構成は以下のようにした。CSSなどをcommon
の中に置いたのは単にデザイナの好みであり、特に意味はない。
wp-content/themes/[テーマ]
├── common
│ ├── css
│ │ ├── [css]
│ │ └── ...
│ ├── img
│ │ ├── [画像]
│ │ └── ...
│ └── js
│ ├── [JS]
│ └── ...
├── elements
│ ├── controllers
│ │ ├── [コントローラ]
│ │ └── ...
│ ├── models
│ │ └── [モデル]
│ │ └── ...
│ └── views
│ ├── [ビュー]
│ └── ...
├── libs
│ ├── functions.php
│ └── helper.php
├── style.css
├── functions.php
├── header.php
├── footer.php
├── sidebar.php
├── index.php
├── category.php
├── single.php
├── page.php
├── ...
└── tests
├── [テスト]
├── ...
└── fixtures
├── [フィクスチャ yamlなど]
└── ...
functions.php
ディレクトリ直下のfunctions.php
にはこれしか書いていない。必要なファイルを読み込む。
<?php
add_theme_support('post-thumbnails', array('post')); // アイキャッチを有効にする
require_once dirname(__FILE__) . '/libs/functions.php';
require_once dirname(__FILE__) . '/libs/helper.php';
libs/functions.php
グローバルに使いたい関数はこっちに置くことにした。get_element()
がコントローラ'sコントローラ(っていう呼び方でいいのかわからないけど)になって、各エレメントのコントローラを実行する。
<?php
require_once dirname(__FILE__) . '/../elements/controllers/element-controller.php';
function get_element($target) {
require_once dirname(__FILE__) . '/../elements/controllers/' . $target . '.php';
$controller = MvcInflector::camelize($target) . 'Controller';
$obj = new $controller;
$obj->exec();
}
// 数個のfunctionが続く。h()とか、is_product()とか、デバッグ用のやつとか。
function h($str) {
return htmlspecialchars($str);
}
...
ちなみにMvcInflector
クラスはWordPressプラグイン開発の偉大なる救世主、WP-MVCで提供されているクラス。
index.php(テンプレート)
index.php
やsingle.php
などのテンプレートファイルは以下のようになっている。get_element()
を多用してるけど、スッキリ書けるようになった。ロジックは絶対に置いてやらない。header.php
やsidebar.php
なんかも同じような構成になっている。
<?php get_header(); ?>
<div id="container" class="pie bg-top">
<div id="main" class="main clearfix">
<div class="left-area clearfix" id="contents">
<?php get_element('common-maintenance'); ?>
<div class="top clearfix">
<?php get_element('top-feature'); ?>
<?php get_element('top-news'); ?>
</div>
<?php get_element('top-recommend'); ?>
<?php get_element(...); ?>
<!-- その他いろんなエレメントが続く... -->
</div>
<?php get_sidebar(); ?>
<?php get_element('footer-banner'); ?>
</div>
</div>
<?php get_footer(); ?>
以下、get_element('top-news')
が呼び出された時の流れを例にする。
elements/controllers/top-news.php(コントローラ)
各エレメントのためのロジックを書く場所。厳密な意味でのコントローラではないのかもしれないけど、便宜上Controllerの名前を冠した。
<?php
class TopNewsController extends ElementController {
public $uses = array(
'Post',
);
public function exec() {
$this->set('posts', $this->Post->find());
$this->render_view('top-news');
}
}
elements/controllers/element-controller.php(コントローラ親クラス)
各コントローラが継承する親クラス。モデルの読み込みとかビューで使うオブジェクトのアサイン、レンダリングなんかをしている。abstractって初めて使った。
<?php
abstract class ElementController {
public $view_vars = array();
public $uses = array();
public function __construct() {
foreach ($this->uses as $model) {
$filename = str_replace('_', '-', strtolower($model));
require_once dirname(__FILE__) . '/../models/' . $filename . '.php';
$this->$model = new $model;
}
}
abstract public function exec();
public function set($name, $obj) {
$this->view_vars[$name] = $obj;
}
public function render_view($view) {
extract($this->view_vars);
require dirname(__FILE__) . '/../views/' . $view . '.php';
}
}
elements/models/post.php(モデル)
コントローラで$this->Post->find()
みたいに書きたかったので、投稿やカテゴリなどWordPress内のオブジェクトを使うときもこんなモデルを噛ませることにした。まだあまり使い込んでない(=作り込んでない)部分なので未熟な感じだけど、とりあえず。独自のモデルを使うときもこんな感じにしたい。
<?php
class Post {
public function find($options=array()) {
$default = array(
'post_status' => 'publish',
'orderby' => 'post_date',
'order' => 'desc',
'posts_per_page' => 10,
'category' => null,
'exclude' => null,
);
$args = array_merge($default, $options);
return get_posts($args);
}
public function fine_one($options=array()) {
$posts = $this->find($options);
return $posts[0];
}
}
elements/views/top-news.php(ビュー)
コントローラでアサインしたオブジェクトを使う。ヘルパーはlibs/helper.php
に書いてある。ヘルパー関数書くのすごく楽しい。
<div id="news-area">
<h2>What's New</h2>
<p class="date"><?php echo date_i18n("m/d"); ?></p>
<ul>
<?php foreach($posts as $key => $val) { ?>
<li class="clearfix">
<p class="image"><?php echo Helper::thumbnail($val->ID, array('url' => get_permalink($val->ID), 'alt' => $val->post_title)); ?></p>
<p class="title"><?php echo Helper::link(summarize($val->post_title, 25), get_permalink($val->ID)); ?></p>
</li>
<?php } ?>
</ul>
</div>
以上のような仕組みでエレメント単位でMVC形式の実装をできるようにした。これでやっとテストが書ける。
ユニットテスト
もちろんPHPUnitを利用する。と言っても恥ずかしながらテストを書くのは初めてなので、テストドメインなどは全く考慮されていないし、その他にもいろいろダメダメだと思う。今後の課題。
phpunit.xml
これはドキュメントルート直下に置いてある。
<phpunit
bootstrap="wp-content/themes/[テーマ名]/tests/bootstrap.php"
backupGlobals="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
>
<testsuites>
<testsuite name="all">
<directory>./wp-content/themes/[テーマ名]/tests/</directory>
</testsuite>
</testsuites>
</phpunit>
tests/bootstrap.php
WordPressをロードしてユニットテストの準備をする。TestCommon.php
については後述。
<?php
define('PHPUNIT', true);
require_once(dirname(__FILE__) . '/../../../../wp-load.php');
require_once(dirname(__FILE__) . '/TestCommon.php');
tests/TopNewsTest.php(ユニットテスト)
ユニットテストの例。TestCommon::getElement()
を使って指定したエレメントのレンダリング結果を取得し、テストする。実際はフィクスチャを使ってデータを突っ込んでからテストしたりしている。下の例ではassertTag()
を使ってるけど、使いずらいのでSeleniumにしたい。今後の課題2。
<?php
class TopNewsTest extends PHPUnit_Framework_TestCase {
public function testView() {
$actual = TestCommon::getElement('top-news');
$matcher = array(
'tag' => 'ul',
'children' => array(
'count' => 10,
'only' => array('tag' => 'li'),
),
);
$this->assertTag($matcher, $actual);
}
// その他のテスト...
}
他にもコントローラが複雑なところなどはTopNewsControllerTest.php
みたいなのを作ってテストしている。
tests/TestCommon.php
get_element()
をラッピングしてテストで使えるようにしている。無理やり感満載なところ。
<?php
class TestCommon {
public static function getElement($target) {
ob_start();
get_element($target);
$buf = ob_get_contents();
ob_end_clean();
return $buf;
}
// その他共通で使えそうなメソッド...
}
テスト実行
ドキュメントルートでphpunit
を一閃。
➜ phpunit
PHPUnit 4.2.2 by Sebastian Bergmann.
Configuration read from /var/www/wordpress/phpunit.xml
............................................................... 63 / 160 ( 39%)
............................................................... 126 / 160 ( 78%)
..................................
Time: 11.68 seconds, Memory: 53.25Mb
OK (160 tests, 191 assertions)
まとめ
以上のような方法でテーマをMVCで書いてユニットテストを書けるようにした。オレオレ感はぬぐいきれないけど、functions.php
にベタ書きしていた時に比べて格段に開発しやすくなったので、結構気に入っている。フレームワークを使えば手間暇かけてこんな構成を考えなくてもいいはずなんだけど、大人の事情でWordPress上で実装しなきゃいけないので仕方がない。
テストを書き始めて1ヶ月弱、現在 160 tests, 191 assertions
。テストを書くのは確かに手間だけど、一旦慣れると今までブラウザでちまちま確認してたのがバカみたいに思えてくる。開発終了までに何個まで数を伸ばせるか楽しみなところ。
次回はCI-as-a-ServiceのWerckerを使って、このプロジェクトを自動テストする手順を書こうと思います。