【PHP】引数にインスタンスを自動に入れる

DI(依存性注入)という処理があるのですが、
コンストラクタの引数にオブジェクトを入れて、疎結合にする処理です。
インターフェースが共通になら別のクラスに入れ替えても中の処理を書き換えることがないので、
テスト時はこっちのクラスで本番はこっちのクラスなどという書き換えが柔軟に行えます。
でもまあクラスが多くなってくると変数に放り込んでいくのもめんどくさいので、勝手にやってくないかなあという話です。

インターフェース共通と言っておきながらこの処理ではクラス名でないと動きません。

[php]
class A {
//コンストラクタの引数をDIする
public function __construct () {
var_dump(‘construct A’);
}
public function info ($from) {
var_dump(‘function info:’.get_class($this) . ‘ from ‘.$from);
}
}

class B {
public function __construct (A $a) {
var_dump(‘construct B’);
$a->info(‘B’);
}
public function info ($from) {
var_dump(‘function info:’.get_class($this) . ‘ from ‘.$from);
}
}
class C {
public function __construct (A $a, B $b) {
var_dump(‘construct C’);
$a->info(‘C’);
$b->info(‘C’);
}
public function info ($from) {
var_dump(‘function info:’.get_class($this) . ‘ from ‘.$from);
}
}
class D {
public function __construct (A $a, B $b, C $c, $hoge = ‘D’) {
var_dump(‘construct D’);
$a->info(‘D’);
$b->info(‘D’);
$c->info(‘D’);
var_dump(‘var $hoge = ‘ . $hoge);
}
public function info ($from) {
var_dump(‘function info:’.get_class($this) . ‘ from ‘.$from);
}
public function success () {
var_dump(‘success DI’);
}
}
class E {
public function __construct () {}
public function di (A $a, B $b, C $c, $hoge = ‘E’) {
var_dump(‘construct E’);
$a->info(‘E’);
$b->info(‘E’);
$c->info(‘E’);
var_dump(‘var $hoge = ‘ . $hoge);
}
public function info ($from) {
var_dump(‘function info:’.get_class($this) . ‘ from ‘.$from);
}
}
class F {
public function __construct () {}
public function di (E $e) {
var_dump(‘construct F’);
$e->info(‘F’);
}
public function info ($from) {
var_dump(‘function info:’.get_class($this) . ‘ from ‘.$from);
}
public function success () {
var_dump(‘success DI’);
}
}
[/php]

こんな感じのクラスがあるとします。

[php]
$d = new D($a, $b, $c, $hoge);
[/php]
Dには引数が4つあって、うち3つはオブジェクトを入れないといけないため、$a,$b,$cをまた別個に…とやっていくと、先の定義からして結構な量書かないといけなくなるうえに入り組んででめんどくさいです。
(ほんとはこんな処理書くなよって話ですし、本番では多分書きはしませんが)
そこで次の感じの関数を投入します。

[php]
/**
* 指定クラスのメソッドが引数を持っているかどうか
**/
function has_params ($class_name, $method_name = ‘__construct’) {
$class = new ReflectionClass($class_name);
$constructor = $class->getMethod($method_name);
return $constructor->getNumberOfParameters();
}
/**
* 指定クラスのメソッドから変数名を取得する
**/
function get_params_from_class_method ($class_name, $method_name = ‘__construct’) {
$class = new ReflectionClass($class_name);
//コンストラクタの引数
$constructor = $class->getMethod($method_name);
$params = [];
foreach ($constructor->getParameters() as $key => $val) {
if (!is_null($val->getClass())) {
$params[] = array(‘is_obj’=>true, ‘class_name’=>$val->getClass()->name);
} else {
//クラス以外の引数、デフォルト値があるの前提で行かないとバグる
$params[] = array(‘is_obj’=>false, ‘is_default’=>$val->isDefaultValueAvailable(), ‘value’=>(($val->isDefaultValueAvailable())?($val->getDefaultValue()):null));
}
}
return $params;
}
//頭から確認していく
function check_params ($class_name, $init_method_name = ‘__construct’) {
if (‘__construct’!==$init_method_name) {
//コンストラクタ以外のメンバ関数を指定している場合、その関数があるかどうか
//ない場合は強制でコンストラクタの処理にする
$class_obj = new ReflectionClass($class_name);
if ($class_obj->hasMethod($init_method_name)) {
$method_name = $init_method_name;
} else {
$method_name = ‘__construct’;
}
} else {
$method_name = ‘__construct’;
}
if (0===has_params($class_name, $method_name)) {
//引数がない場合そのまま返す
$class_obj = new $class_name();
return $class_obj;
} else {
//引数がある場合
//指定クラスのメソッドから変数名を取得する
$params = get_params_from_class_method($class_name, $method_name);
//代入する変数
$param_array = [];
//取得した変数をチェック
foreach ($params as $param) {
if (!$param[‘is_obj’]) {
//通常はデフォルトの値があると思うが順番がおかしかったりするとないかもしれない
//その場合はnullが入るのでエラーになるかもしれない
$param_array[] = $param[‘value’];
} else {
//クラス名が取得できた場合、オブジェクトにして返す
//2巡目からはコンストラクタ固定なのでメソッド名はデフォルトの値で処理する
$param_array[] = check_params($param[‘class_name’], $init_method_name);
}
}
//引数がそろったところでそれらをすべて入れてオブジェクトを作成する
$class_obj = new ReflectionClass($class_name);
if (‘__construct’===$method_name) {
return $class_obj->newInstanceArgs($param_array);
} else {
$obj = $class_obj->newInstance();
call_user_func_array(array($obj, $method_name), $param_array);
return $obj;
}
}
}
[/php]

関数にしてますがメモリ上無駄な処理も入ってるのでクラスにまとめたほうがいいかと。
そしてDを作る処理は

[php]
$class_name = ‘D’;
//Dのインスタンス
$d = check_params(‘D’);
//Dのメンバ関数を実行
$d->success();
[/php]

これでOKになります。
注意としては普通の変数を引数に取るコンストラクタの場合は、デフォルト引数がないとNULLが入っちゃいます。
コンストラクタで依存性を注入することをコンストラクタインジェクションというそうです。
一方メソッドで依存性を注入することはメソッドインジェクションというみたいです。
セッターで入れる場合にはセッターインジェクションです。
んじゃクラスFのメソッドdiで依存性を注入する場合には上の関数では、

[php]
$class_name = ‘F’;
$f = check_params(‘F’, ‘di’);
$f->success();
[/php]

になります。
この場合には、F::di()に依存性注入のための解決を図ってますが、その下ではdiがあればdi関数を実行して依存性の注入、
ない場合にはコンストラクタに依存性の注入を行っています。
そのためコンストラクタと依存性注入するメソッド両方に引数があるとうまく動かないと思います。

セッターインジェクションは、セッター用意してる時点でDIっていうかカプセル化っていうかみたいな感じなので、
↑の方法では処理できないかと思います。
setXXX名のメンバ関数を探し出して放り込んでいくって感じで良いと思いますが、
この方法ほかの言語ならともかくPHPだとメンバ関数が増えまくって微妙なのでここでは割愛で。

ここではどのクラスも引数をそのまま捨ててますが、普通は受け取ったらメンバ変数に入れると思うので使う用ならそうしてください。
という感じのまた役に立つかどうかわからない処理を。