いけむランド

はてダからやってきました

PHP でパッケージプライベートを実現してみる

他にも何とでもやりようはありそうだけど、とりあえず思いついた方法で実装してみた。

ちなみにここでパッケージプライベートというのは同じ名前空間からのみアクセスできるという意味で言っている。

参考にしたのは以下の記事である。

<?php
namespace
{
    class PackagePrivateImplementor
    {
        private $nameSpaceName;

        public function __construct($nameSpaceName)
        {
            $this->nameSpaceName = $nameSpaceName;
        }

        function __call($name, $arguments)
        {
            if (!method_exists($this, '_' . $name)) {
                throw new \Exception('No such method: ' . $name);
            }
            $backTrace = debug_backtrace();
            if (count($backTrace) < 3) { // main からの呼び出し
                throw new \Exception('Cannot invoke package private method from main!');
            } else {
                $callerTrace = $backTrace[2];
            }
            $name = '_' . $name;
            if (array_key_exists('class', $callerTrace)) {
                $caller = new \ReflectionClass($callerTrace['class']);
            } else if (array_key_exists('function', $callerTrace)) {
                $caller = new \ReflectionFunction($callerTrace['function']);
            } else {
                return; // ここに到達することはないはず...
            }
            $callerNamespaceName = $caller->getNamespaceName();
            if ($callerNamespaceName != $this->nameSpaceName) { // 呼び出し元が呼び出し先と同じ名前空間の場合のみ呼べる
                throw new \Exception('Cannot invoke package private method from other package method!');
            }
            $calleeMethod = new \ReflectionMethod($this, $name);
            $calleeMethod->setAccessible(true);
            $calleeMethod->invokeArgs($this, $arguments);
            $calleeMethod->setAccessible(false);
        }
    }
}

namespace foo\bar
{
    class Loneliness extends \PackagePrivateImplementor
    {
        public function __construct()
        {
            parent::__construct(__NAMESPACE__);
        }

        private function _packagePrivateMethod($arg1, $arg2)
        {
            echo sprintf('This is package private method : arg1=%s, arg2=%s', $arg1, $arg2) . PHP_EOL;
        }
    }

    class Friend
    {
        static function invokePackagePrivateMethod($arg1, $arg2)
        {
            $loneliness = new Loneliness();
            $loneliness->packagePrivateMethod($arg1, $arg2);
        }
    }
    
    function invokePackagePrivateMethod($arg1, $arg2)
    {
        $loneliness = new Loneliness();
        $loneliness->packagePrivateMethod($arg1, $arg2);
    }
}

namespace foo\baz
{
    class NotFriend
    {
        static function invokePackagePrivateMethod($arg1, $arg2)
        {
            $loneliness = new \foo\bar\Loneliness();
            $loneliness->packagePrivateMethod($arg1, $arg2);
        }
    }

    function invokePackagePrivateMethod($arg1, $arg2)
    {
        $loneliness = new \foo\bar\Loneliness();
        $loneliness->packagePrivateMethod($arg1, $arg2);
    }
}

namespace
{
    // 同じ名前空間の別のメソッドから呼ぶ
    foo\bar\Friend::invokePackagePrivateMethod(11, 12);

    // 同じ名前空間の別の関数から呼ぶ
    foo\bar\invokePackagePrivateMethod(21, 22);

    // 別の名前空間の別のメソッドから呼ぶ
    try {
        foo\baz\NotFriend::invokePackagePrivateMethod(31, 32);
    } catch (Exception $e) {
        echo $e->getMessage() . PHP_EOL;
    }

    // 別の名前空間の別の関数から呼ぶ
    try {
        foo\baz\invokePackagePrivateMethod(41, 42);
    } catch (Exception $e) {
        echo $e->getMessage() . PHP_EOL;
    }

    // main から呼ぶ
    try {
        $loneliness = new \foo\bar\Loneliness();
        $loneliness->packagePrivateMethod(51, 52);
    } catch (Exception $e) {
        echo $e->getMessage() . PHP_EOL;
    }
}


実行結果は以下のとおりである。

This is package private method : arg1=11, arg2=12
This is package private method : arg1=21, arg2=22
Cannot invoke package private method from other package method!
Cannot invoke package private method from other package method!
Cannot invoke package private method from main!


動作の説明と考察みたいなものを以下にまとめる。

  • 今回の例ではアンスコつきのメソッド名でパッケージプライベートにしたいメソッドを定義し、呼び出される時はアンスコなしのメソッド名で呼ぶというルールにしている。
    • アンスコなしメソッドで呼ばれた時に __call() で解決させる。
    • そのような命名規則を使用したくない場合はアノテーションを使うという方法もあると思う。
    • そもそも __call() である必要もない。引数を配列で受け取る __call() の代替メソッド (仮に verifyAndInvoke() としてみる) を用意して、以下のようにアンスコなしのメソッドを定義すればいい。この場合は IDE で補完が効く反面、パッケージプライベートなメソッドの数だけ、以下のような定義が必要となる。
  public function packagePrivateMethod($arg1, $arg2)
  {
    $this->verifyAndInvoke(func_get_args());
  }
  • debug_backtrace() からコールスタックを取得する。
    • コールスタックはざっくり言うと以下のようになっている。 
添字 コンテキスト
0 __call
1 packagePrivateMethod
2 呼び出し元
    • main からの呼び出しである場合はパッケージ外と判断させるため、あらかじめコールスタック長が 3 以上か判定している。(main の場合は呼び出し元のコールスタックが存在しない。)
    • メソッドか関数に名前空間の情報があるため、それを取り出し、自分の名前空間と同じか判別する。
    • 同じである場合はリフレクションでマップされているメソッドを呼ぶ。
  • この例ではパッケージプライベートの判定を名前空間でしているが、ファイルパスで判断することもできる。
    • コールスタックに絶対ファイルパスも入っているため、それの dirname() で判別すれば良いはずである。