Laravelで__callと__callStaticを使った静的メソッドと動的メソッドの相互呼び出しについて

LaravelのORMがなぜ便利なのかを理解したいと思っていましたが、仕事が忙しくてなかなか時間が取れませんでした。最近自分で何かを作っている際に、呼び出し方を間違えないようにするためにソースコードを調べてみました。

$counter = AdminLog::where('is_delete', GlobalCode::NORMAL);

この静的呼び出しを追跡すると、このクラスは実際には動的メソッドであることがわかりました。

    public function where($column, $operator = null, $value = null, $boolean = 'and')
    {
        if ($column instanceof Closure && is_null($operator)) {
            $column($query = $this->model->newQueryWithoutRelationships());

            $this->query->addNestedWhereQuery($query->getQuery(), $boolean);
        } else {
            $this->query->where(...func_get_args());
        }

        return $this;
    }

継承されたモデルのModelが呼び出すBuilderクラスの__callメソッドを見てみましょう。

    public function __call($method, $parameters)
    {
        if (in_array($method, ['increment', 'decrement'])) {
            return $this->$method(...$parameters);
        }

        if ($resolver = (static::$relationResolvers[get_class($this)][$method] ?? null)) {
            return $resolver($this);
        }

        return $this->forwardCallTo($this->newQuery(), $method, $parameters);
    }

    /**
     * Handle dynamic static method calls into the model.
     *
     * @param  string  $method
     * @param  array  $parameters
     * @return mixed
     */
    public static function __callStatic($method, $parameters)
    {
        return (new static)->$method(...$parameters);
    }

Builderクラスの__callメソッドは以下のようになっています。

public function __call($method, $parameters)
    {
        if ($method === 'macro') {
            $this->localMacros[$parameters[0]] = $parameters[1];

            return;
        }

        if ($this->hasMacro($method)) {
            array_unshift($parameters, $this);

            return $this->localMacros[$method](...$parameters);
        }

        if (static::hasGlobalMacro($method)) {
            $callable = static::$macros[$method];

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

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

        if ($this->hasNamedScope($method)) {
            return $this->callNamedScope($method, $parameters);
        }

        if (in_array($method, $this->passthru)) {
            return $this->toBase()->{$method}(...$parameters);
        }

        $this->forwardCallTo($this->query, $method, $parameters);

        return $this;
    }

    /**
     * Dynamically handle calls into the query instance.
     *
     * @param  string  $method
     * @param  array  $parameters
     * @return mixed
     *
     * @throws \BadMethodCallException
     */
    public static function __callStatic($method, $parameters)
    {
        if ($method === 'macro') {
            static::$macros[$parameters[0]] = $parameters[1];

            return;
        }

        if ($method === 'mixin') {
            return static::registerMixin($parameters[0], $parameters[1] ?? true);
        }

        if (! static::hasGlobalMacro($method)) {
            static::throwBadMethodCallException($method);
        }

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

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

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

核心となるのは__callと__callStaticの相互利用です。

    public function __call($method, $parameters)
    {
        return $this->$method(...$parameters);
    }

    public static function __callStatic($method, $parameters)
    {
        return (new static)->$method(...$parameters);
    }

これが核心部分です。

例を見てみましょう。

test.php

<?php

include_once './p.php';
include_once './pp.php';

$a = new pp();
$a->notify(1111);
echo PHP_EOL;
//$a->aa(1111);

//pp::notify(1111);

p.php 親クラス

<?php

class p
{
    public function notify($ddd)
    {
        print_r(get_class($this) . 'notify111111111');
        echo PHP_EOL;
    }

    public static function refund()
    {
        echo self::class . 'refund';
    }
    public function __call($method, $parameters)
    {
        return $this->$method(...$parameters);
    }

    public static function __callStatic($method, $parameters)
    {
        return (new static)->$method(...$parameters);
    }
}

pp.php 子クラス

<?php

class pp extends p
{
    // public function notify($ddd)
    // {
    //     print_r(get_class($this) . 'notify111111111');
    //     echo PHP_EOL;
    // }

    // public static function refund()
    // {
    //     echo self::class . 'refund';
    // }

}

注意点:

  1. 親クラスと子クラスを継承する場合、子クラスのメソッドを呼び出すときは、子クラスのメソッドはprotectedまたはそれ以下のアクセス権でなければなりません。そうしないと警告が表示されます。

  2. 親クラスと子クラスを継承する場合、メソッドが親クラスのメソッドである場合、親クラスは警告メッセージを表示しません。

  3. 子クラスまたは現在のクラスにpublicメソッドがある場合、まずpublicメソッドを探し、見つからない場合は__callや__callStaticマジックメソッドを呼び出します。

  4. このような設計にする必要がある場合は、クラスのアクセス権に注意してください。さもないと多くの小さな問題が発生します。

タグ: laravel PHP マジックメソッド eloquent ORM

5月26日 09:43 投稿