LoginSignup
13
13

More than 5 years have passed since last update.

private, protected なコンストラクタを外部からコールする方法

Last updated at Posted at 2013-12-19

導入

private, protected なコンストラクタを特別に許可した外部クラスからコールしたいとき。

「 RefletcionClass 使えばいけるんじゃねーの?」

って思うかもしれませんが・・・

ss (2013-12-19 at 04.22.00).png

ss (2013-12-19 at 04.22.50).png

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択!!!

13
13
3

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
13
13