Help us understand the problem. What is going on with this article?

WordPressのテーマをMVCでテスタブルに書く

More than 5 years have passed since last update.

もろもろの事情から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にはこれしか書いていない。必要なファイルを読み込む。

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コントローラ(っていう呼び方でいいのかわからないけど)になって、各エレメントのコントローラを実行する。

libs/functions.php
<?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.phpsingle.phpなどのテンプレートファイルは以下のようになっている。get_element()を多用してるけど、スッキリ書けるようになった。ロジックは絶対に置いてやらない。header.phpsidebar.phpなんかも同じような構成になっている。

index.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の名前を冠した。

elements/controllers/top-news.php
<?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って初めて使った。

elements/controllers/element-controller.php
<?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内のオブジェクトを使うときもこんなモデルを噛ませることにした。まだあまり使い込んでない(=作り込んでない)部分なので未熟な感じだけど、とりあえず。独自のモデルを使うときもこんな感じにしたい。

elements/models/post.php
<?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に書いてある。ヘルパー関数書くのすごく楽しい。

elements/views/top-news.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.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 については後述。

tests/bootstrap.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。

tests/TopNewsTest.php
<?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()をラッピングしてテストで使えるようにしている。無理やり感満載なところ。

tests/TestCommon.php
<?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を使って、このプロジェクトを自動テストする手順を書こうと思います。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした