LoginSignup
6
6

More than 5 years have passed since last update.

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

Last updated at Posted at 2014-10-20

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

やりたかったこと

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

詳しくは、ウィキペディア日本語版の制御の反転 - Wikipediaなどを参照されたい。

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

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

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

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

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6