導入
private, protected なコンストラクタを特別に許可した外部クラスからコールしたいとき。
「 RefletcionClass 使えばいけるんじゃねーの?」
って思うかもしれませんが・・・
ReflectionMethod::invoke() の $object
に何渡したらいいねん、ってなりますよね。コンストラクタをコールする段階ではまだインスタンスを作っていないので、この方法は使えません。ではどうするか・・・?
解決策1: 自前でシリアルデータを書く
private とか protected なときに NULL文字 使って変なフォーマットにしなければいけないので結構めんどくさいです。
ここでは簡単のためにメソッドではなく関数として表現します。実際にクラスに組み込むときには function
ではなくて private static function
等にしてください。
function getInstance($class, array $properties = array()) {
$r = new ReflectionClass($class);
if (
$r->isAbstract() ||
$r->isInterface() ||
version_compare(PHP_VERSION, '5.4.0', '>=') && $r->isTrait()
) {
throw new InvalidArgumentException("{$r->name} is not instantiable");
}
$serials = array();
foreach ($r->getProperties() as $rp) {
if ($rp->isStatic() || !isset($properties[$rp->name])) {
continue;
}
if ($rp->isPrivate()) {
$serials[] = serialize("\0{$r->name}\0{$rp->name}");
} elseif ($rp->isProtected()) {
$serials[] = serialize("\0*\0{$rp->name}");
} else {
$serials[] = serialize($rp->name);
}
$serials[] = serialize($properties[$rp->name]);
}
return unserialize(sprintf(
'O:%d:"%s":%d:{%s}',
strlen($r->name),
$r->name,
count($serials) / 2,
implode('', $serials)
));
}
class Test {
public $a = 'default';
protected $b = 'default';
private $c = 'default';
public $d = 'default';
private function __construct() { }
}
var_dump(getInstance(
'Test',
array_fill_keys(range('a', 'c'), 'new')
));
object(Test) ... (4) {
["a"]=>
string(3) "new"
["b":protected]=>
string(3) "new"
["c":"Test":private]=>
string(3) "new"
["d"]=>
string(7) "default"
}
この方法だと実際にはコンストラクタをコールしていないため、プロパティへのセット以外の操作をコンストラクタ相当に行いたい場合には対応しきれないケースが発生してきます。それが必要な場合は解決策2に帰着することになります。
解決策2: private static なファクトリメソッドを用意しておく
最初に ReflectionMethod::invoke() が使えないと断念しましたが、そもそもファクトリメソッドがあればいい話です。但し ReflectionMethod::setAccessible() は PHP5.3.2以降 でしか使えません。
function getInstance($class, array $arguments = array()) {
$r = new ReflectionClass($class);
$rm = $r->getMethod('factory');
$rm->setAccessible(true);
return $rm->invoke(null, $arguments);
}
class Test {
public $a = 'default';
protected $b = 'default';
private $c = 'default';
public $d = 'default';
private function __construct() { }
private static function factory(array $properties = array()) {
$object = new self;
$properties = array_intersect_key($properties, get_object_vars($object));
foreach ($properties as $name => $value) {
$object->$name = $value;
}
return $object;
}
}
var_dump(getInstance(
'Test',
array_fill_keys(range('a', 'c'), 'new')
));
object(Test) ... (4) {
["a"]=>
string(3) "new"
["b":protected]=>
string(3) "new"
["c":"Test":private]=>
string(3) "new"
["d"]=>
string(7) "default"
}
解決策3: Closure::bind() と ReflectionClass::newInstanceWithoutConstructor()
suinさんの以下の投稿を参考にさせていただきました。
Qiita - コンストラクタがprivateなクラスを1行でテスト可能にする
やたらメソッド名が長いですが、要は コンストラクタをコールせずにインスタンスを生成する メソッドです。解決策1でやったことで、プロパティをセットすることを除けばこのメソッド1行で代用することが出来ます。また、 Closure::bind() は static なバインドだけでなく、インスタンスに対するバインドも可能です。つまりバインドするクロージャの中で $this
が使えるということです。素晴らしい解決策に思えますが、どちらも PHP5.4.0以降 でしか使えません。
Closure::bind() だけで実現できますが、 class_exists() で調べて例外処理するのが面倒なので、敢えてどちらも併用するようにして失敗したときの処理は任せています。
function superNewInstanceArgs($class, array $arguments = []) {
$object = (new ReflectionClass($class))->newInstanceWithoutConstructor();
call_user_func(closure::bind(
function () use ($arguments) {
call_user_func_array([$this, '__construct'], $arguments);
},
$object,
$object
));
return $object;
}
class Test {
public $a = 'default';
protected $b = 'default';
private $c = 'default';
private function __construct($a, $b, $c) {
$this->a = $a;
$this->b = $b;
$this->c = $c;
}
}
var_dump(superNewInstanceArgs('Test', ['A', 'B', 'C']));
object(Test) ... (3) {
["a"]=>
string(1) "A"
["b":protected]=>
string(1) "B"
["c":"Test":private]=>
string(1) "C"
}
美しいけどバージョンに縛られるのが非常に残念・・・
結局どうすればいいか?
解決策1
PHP5.0.0 とかいうクソ環境でも動く上に、生成される側のクラス自体は汚さないので、個人的には結構好きですね。
解決策2
PHP5.3.2 以降で動くし、まぁいいでしょう。
解決策3
PHP5.4.0 以降しか使わないって決めてる人はこれ1択!!!