PHP で callable なプロパティを呼び出す方法

PHP の小ネタです。 PHP において「 callable なオブジェクトが別のオブジェクトのプロパティとして格納されているときに、それをどのように呼び出せばよいのか」という問題についてです。

具体的に、インスタンスが callable なクラス Registry が定義されている場合を考えてみます。

class Registry
{

  /**
   * マジックメソッド __invoke()
   */
  public function __invoke($name)
  {
    switch ($name) {
      case 'current_user':
        return new User();

      case 'timer':
        return new Timer();

      // ...
    }
  }

}

このクラスのオブジェクトは単体では次のように呼び出して利用することができます。

$registry = new Registry();

$current_user = $registry('current_user');
$timer = $registry('timer');

このクラスのオブジェクトが他のクラスでプロパティにセットされて使用される場合を考えてみましょう。 次の MyController クラスは Registry クラスのオブジェクトを registry プロパティに格納して利用します。

class MyController {

  private $registry;

  /**
   * コンストラクタ
   */
  public function __construct(Registry $registry)
  {
    $this->registry = $registry;
  }

  /**
   * GET リクエストを処理する
   */
  public function run()
  {
    // ...
    $timer = $this->registry('timer');
    // ...
  }

}

使用側のイメージは次のとおりです。

$controller = new MyController(new Registry());
$controller->run();

しかし、これをそのまま実行すると次のようなエラーが出てしまいます。

// Uncaught Error: Call to undefined method MyController::registry()

これは $this->registry() のところが、プロパティ registry__invoke() の呼び出しではなく、 MyController クラスの registry() というメソッドの呼び出しとみなされてしまうためです。 registry() というメソッドは定義していないのでエラーが起こるのは当然といえば当然です。

では、 Registry__invoke() を呼び出したければどのようにすればよいのか、なのですが、 PHP 7 であればプロパティアクセスまでを () で囲うと OK です。

$timer = ($this->registry)('timer');

このように書くと、まず $this->registry というプロパティにアクセスしてその __invoke() を呼んでね、という指定になるからです。

ちなみに、この書き方は PHP 5 ではシンタックスエラーになってしまうため PHP 5 では使えません。 PHP 5 ではよい方法が無いので、同じことをやりたい場合は「いったん変数に代入してから呼び出す」といった方法を採ることになります。

// $timer = $this->registry('timer');
// これを次のように書き換えると動く
$registry = $this->registry;
$timer = $registry('timer');

ただ・・・これではせっかく __invoke() を使っているのが台無しな感じがします。

PHP 5 で使えるもうひとつのアプローチとしては、 call_user_func() または call_user_func_array() を使った方法があります。

// $timer = $this->registry('timer');
// 次のように書き換えても OK
call_user_func($this->registry, 'timer');

ただこれも __invoke() を有効活用できているとはとても言えがたい感じですね。 このやり方で呼び出すぐらいなら普通にメソッドを定義して呼んだ方がましな気がします。

また、 PHP 5 で使えるもうひとつ奥の手として、プロパティを持つクラスの方で __call() を実装して呼び出せるようにするというアプローチがあります。

class MyController {

  private $registry;

  /**
   * コンストラクタ
   */
  public function __construct(Registry $registry)
  {
    $this->registry = $registry;
  }

  /**
   * GET リクエストを処理する
   */
  public function run()
  {

    // ...
    $timer = $this->registry('timer');
    // ...
  }

  /**
   * マジックメソッド __call()
   *
   * callable なプロパティを直接呼び出せるようにする
   */
  public function __call($name, $arguments)
  {
    if (isset($this->{"$name"}) && is_callable($this->{"$name"})) {
      return call_user_func_array($this->{"$name"}, $arguments);
    }

    throw new Exception("メソッド $name の不正な呼び出しがありました。");
  }

}

// $this->registry('timer') が問題なく動作する
$controller = new MyController(new Registry());
$controller->run();

ただし、ここまでやるのは少々やり過ぎな感じがします。 これをやるぐらいなら、 Registry に普通の public なメソッドを定義してそれを呼び出すようにした方が良い気がします。

というわけで、まとめると次のような感じでした。

PHP で callable なプロパティを呼び出す方法:

  • PHP 7
    • プロパティアクセスまでを () で囲えば OK
    • ($obj->prop)(...$args); のように書く
  • PHP 5
    • 原則不可能
    • 迂回作としては「いったんプロパティを変数に代入する」「 call_user_func() で呼び出す」「 __call() を実装する」等がある

というわけで、 PHP のマジックメソッドに関する小ネタでした。