すごい適当に書いた記事だしサンプルコードは一度も実行してないので、SyntaxErrorがあるかもしれにあ。
やりたかったこと
Inversion of Control (IoC) がやりたかった。
詳しくは、ウィキペディア日本語版の制御の反転 - Wikipediaなどを参照されたい。
PHPでこれを実現する手法などは PHPでDI(Dependency Injection) - Qiita にまとめられてるので参照されたいが、いま触ってるプロダクトではDIコンテナを使った手法を適用しにくかったので、自分で書いた次第。
PHP のスタティックメソッドをモック化する - pixiv engineering blog と似てるけど、主眼が若干異なる。モックはテストごとに設定しないと本来の実装が呼び出されるが、それを確実に堰き止められるようにしたい。
例
なんか通信したりするライブラリがあって、静的メソッドのインターフェイスを公開してるとする。
<?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;
}
}
実装
ライブラリにしても良い気がするし、別にしないでコピペしてもいいんじゃないかって気がするので、おのおのコピペすると良いんじゃないかって気がしてる。テスト書いてない。
<?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('そんなメソッドはない');
}
}
<?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);
}
}
利用方法
<?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に適当なデータ突っ込むにしてもめんどくささはあるけど、これを仕事で書いてるプロジェクトにぶちこめるかは結構悩む。 (悩んでるので、この記事で供養してやろうかみたいな気分も若干ある)