Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

More than 5 years have passed since last update.

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

tadsan
僕に警備する自宅をください。Emacs初心者。Rubyist。 全ての投稿された記事は別段の表記がない限りはCC 3.0 BY-SA https://creativecommons.org/licenses/by-sa/3.0/deed.ja で二次利用できます。 記事中に含まれる全てのコードスニペットの著作権は抛棄するので、煮るなり焼くなりお好きにどうぞ。
https://tadsan.github.io/
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