静的メソッドとIoC(制御の反転)

すごい適当に書いた記事だしサンプルコードは一度も実行してないので、SyntaxErrorがあるかもしれにあ。

## やりたかったこと

**Inversion of Control** (IoC) がやりたかった。

詳しくは、ウィキペディア日本語版の[制御の反転 - Wikipedia](http://ja.wikipedia.org/wiki/%E5%88%B6%E5%BE%A1%E3%81%AE%E5%8F%8D%E8%BB%A2)などを参照されたい。

PHPでこれを実現する手法などは [PHPでDI（Dependency Injection） - Qiita](http://qiita.com/lga0503/items/4fcf6dc52d993df5dbb2) にまとめられてるので参照されたいが、いま触ってるプロダクトではDIコンテナを使った手法を適用しにくかったので、自分で書いた次第。

[PHP のスタティックメソッドをモック化する - pixiv engineering blog](http://inside.pixiv.net/entry/2014/02/27/002614) と似てるけど、主眼が若干異なる。モックはテストごとに設定しないと本来の実装が呼び出されるが、それを確実に堰き止められるようにしたい。

## 例

なんか通信したりするライブラリがあって、静的メソッドのインターフェイスを公開してるとする。

```FugaFuga.php
<?php
namespace HogeLib;

class FugaFuga
{
    /**
     * @param  int   $id
     * @param  array $options
     * @return array
     */
    public static function hogeMethod($id, array $options = [])
    {
        //
        // なんか通信したりDB引いたり副作用の大きいめんどくさい処理がある
        //

        // ['hoge' => 'piyo']
        return $result;
    }
}
```

## 実装

ライブラリにしても良い気がするし、別にしないでコピペしてもいいんじゃないかって気がするので、おのおのコピペすると良いんじゃないかって気がしてる。テスト書いてない。

```ServiceBase.php
<?php
namespace Teto\Service;

/**
 * @author  tadsan<tadsan@zonu.me>
 * @license MIT
 */
abstract class SearviceBase
{
    protected static $is_test = false;
    private static $procedures = [];

    final private function __construct(){}
    
    final public static function initTestMode()
    {
        self::$procedures = [];
        self::$is_test = true;
    }

    /**
     * @param string   $func_name
     * @param callable $procedure
     */
    final public static function register($func_name, callable $procedure)
    {
        if (!self::$is_test) {
            throw new \LogicException('テストモード以外でSearviceBase::registerを実行することはできないお');
        }

        $method = self::getReflectionMethod($func_name);
        $class  = $method->getDeclaringClass()->getName();

        if (empty(self::$procedures[$class])) {
            self::$procedures[$class] = [];
        }

        self::$procedures[$class][$func_name] = $procedure;
    }

    final public static function __callStatic($name, $args)
    {
        return call_user_func_array(self::$procedures[(new \ReflectionClass(new static))->getName()][$name], $args);
    }

    final private static function getReflectionMethod($func_name)
    {
        $ref = new \ReflectionClass(new static);

        foreach ($ref->getMethods() as $method) {
            if ($method->getName() === $func_name) {
                return $method;
            }
        }

        throw new \LogicException('そんなメソッドはない');
    }
}
```

```HogeService.php
<?php
namespace My\Service;

final class HogeService extends Teto\Service\SearviceBase
{
    /**
     * @param  int   $id
     * @param  array $options
     * @return array
     */
    public static function hogeMethod($id, array $options = [])
    {
        if (!parent::$is_test) {
            return HogeLib\FugaFuga::hogeMethod($id, $options);
        }

        return parent::hogeMethod($id, $options);
    }
}
```

## 利用方法

```hoge.php
<?php
use My\Service\HogeService;

$id = 666;
$option = ['fizz' => true, 'buzz' => false];

var_dump(HogeService::hogeMethod($id, $option));
// => ['hoge' => 'piyo']

// テストモードを有効にします
\Teto\SearviceBase::initTestMode();

try {
    HogeService::hogeMethod($id, $option);
} catch (\Exception $e) {
// めんどくさいので実際に試してないよ
// Warning: call_user_func_array() expects parameter 1 to be a valid callback,
//          no array or string given in ...
}

HogeService::register('hogeMethod',
    function ($id, array $options = []) { return ['hoge' => 'fuga']; }
);

var_dump(HogeService::hogeMethod($id, $option));
// => ['hoge' => 'fuga']
```

実際にはPHPUnitとかで利用して、自前の`TestCase::setUp()`で`\Teto\SearviceBase::initTestMode();`で初期化するようにしてやると便利かもしれない。

## 最後に

前提にある「なんか通信したりするライブラリがあって、静的メソッドのインターフェイスを公開してるとする」って状況が結構限定的な気もしないでもない。

モック作るにしてもテストDBに適当なデータ突っ込むにしてもめんどくささはあるけど、これを仕事で書いてるプロジェクトにぶちこめるかは結構悩む。 *(悩んでるので、この記事で供養してやろうかみたいな気分も若干ある)*