MacroableでPHPのオーバーロードを学ぶ

PHPのオーバーロードを説明する上でLaravelのMacroableがちょうど良いと思ったので。

マクロについて

Laravel内部の一部のクラスには、独自に定義したメソッドを追加できる「マクロ」という機能が備わっています。

マクロを追加したいクラスにMacroableをuse宣言することによって、当該クラスにマクロを登録することが出来ます。
例えば Illuminate\Support\Collection はMacroableをuseしているので、以下のようにしてマクロの登録が行えます。

public function boot()
{
    Collection::macro('foo', function() {
        $this->map(function($value) {
            // 処理
        });
    });
}

よくあるマクロの使いどころとしては、CollectionResponse といったFacadesに対して、汎用的に再利用したい処理がある場合などかと思っています。

このマクロはPHPのオーバーロードを利用して実現されています。

オーバーロードについて

PHPにおけるオーバーロードとは、プロパティやメソッドを動的に作成する手法であり、マジックメソッドを利用します。また、オーバーロードメソッドは宣言されていないプロパティやメソッドが呼び出された際に起動します。

PHP: オーバーロード - Manual

未宣言のメソッドに関しては __call というマジックメソッドを用意しておくことで起動出来ます (staticは __callStatic )。

// __callを宣言していないクラス
class Bar {}

$bar = new Bar();
$bar->bar(); // PHP Fatal error:  Uncaught Error: Call to undefined method Bar::bar()

// __callを宣言したクラス
class Foo {
    public function __call($method, $parameters) {
        echo 'Method: ' . $method;
    }
}

$foo = new Foo();
$foo->foo(); // Method: foo

また、同名で引数の型や数の異なるメソッドを宣言する(一般的な)オーバーロードとは解釈が異なります。

コードリーディング

実際に以下の流れでコードを見ていきます。

  1. マクロを登録する
  2. マクロを呼び出す

マクロを登録する

まずはマクロの登録時の処理を見ていきます。登録時の処理はMacroableに定義されています。

登録時に呼び出すmacroメソッドですが、第一引数の呼び出す際のメソッド名をkeyに、第二引数のクロージャを valueとして $macro というプロパティに詰め込んでいます。

/**
 * The registered string macros.
 *
 * @var array
 */
protected static $macros = [];

/**
 * Register a custom macro.
 *
 * @param  string  $name
 * @param  object|callable  $macro
 * @return void
 */
public static function macro($name, $macro)
{
    static::$macros[$name] = $macro;
}

冒頭で例に上げたCollectionクラスは、Macroableをuseしていますので、 Collection::macro を呼び出すことで、$macro にメソッド名 foo とクロージャが登録されることになります。

public function boot()
{
    Collection::macro('foo', function() {
        $this->map(function($value) {
            // 処理
        });
    });
}

マクロを呼び出す

登録したマクロはメソッド名を -> または :: で呼び出すことで利用が出来ます。

// インスタンス化して呼び出す
collect()->foo();

// staticメソッドとして呼び出す
Collection::foo();

このとき、Collectionにfooメソッドは宣言されていないのでオーバーロードされ、該当するマジックメソッドが呼び出されます。

/**
 * Dynamically handle calls to the class.
 *
 * @param  string  $method
 * @param  array  $parameters
 * @return mixed
 *
 * @throws \BadMethodCallException
 */
public static function __callStatic($method, $parameters)
{
    if (! static::hasMacro($method)) {
        throw new BadMethodCallException(sprintf(
            'Method %s::%s does not exist.', static::class, $method
        ));
    }

    $macro = static::$macros[$method];

    if ($macro instanceof Closure) {
        $macro = $macro->bindTo(null, static::class);
    }

    return $macro(...$parameters);
}

/**
 * Dynamically handle calls to the class.
 *
 * @param  string  $method
 * @param  array  $parameters
 * @return mixed
 *
 * @throws \BadMethodCallException
 */
public function __call($method, $parameters)
{
    if (! static::hasMacro($method)) {
        throw new BadMethodCallException(sprintf(
            'Method %s::%s does not exist.', static::class, $method
        ));
    }

    $macro = static::$macros[$method];

    if ($macro instanceof Closure) {
        $macro = $macro->bindTo($this, static::class);
    }

    return $macro(...$parameters);
}

__call でも __callStatic でも概ねやっていることは同じで、

  1. hasMacro で呼び出されたメソッド名がマクロとして登録されているか($macroに該当するメソッド名が存在するか)を検証する
    • 存在しなければBadMethodCallExceptionをthrowする
  2. クロージャをバインドする
    • __call : 呼び出されたメソッドを持つオブジェクトにクロージャをバインドする
    • __callStatic : バインドを解除する
  3. 当該メソッドを引き渡されたパラメータで実行する

という流れになります。

タイトルとURLをコピーしました