/dev/null

脳みそのL1キャッシュ

Eloquent Model 内の処理の委譲

はじめに

Eloquent Model の create メソッ の実装を見てやろうと Laravel Framework のコードを潜ってみたところ、なかなか面白い処理フローになっていたのでメモ

TL;DR

  • Model::create() を実行すると処理が Builder::create() に委譲される
  • 処理の委譲は __call() メソッドと ForwardCalls トレイトを使って実現される

__call()

https://laravel.com/api/8.x/Illuminate/Database/Eloquent/Model.html を見たらわかると思うが Model クラスには create() メソッドが存在しない。

また、Model クラスは他のクラスを継承していないし、use しているトレイトにもそれらしいメソッドはない。では、Model::create を実行したときに一体なにが呼び出されるのか。

実は Model クラスは __call() メソッドを実装していて、定義されていないメソッドを呼び出すと __call() メソッドに処理が移る

abstract class Model implements Arrayable, ArrayAccess, Jsonable, JsonSerializable, QueueableEntity, UrlRoutable
{
    use Concerns\HasAttributes,
        Concerns\HasEvents,
        Concerns\HasGlobalScopes,
        Concerns\HasRelationships,
        Concerns\HasTimestamps,
        Concerns\HidesAttributes,
        Concerns\GuardsAttributes,
        ForwardsCalls; // forwardCallTo() が実装されている

    ...(snip)...

    /**
     * Handle dynamic method calls into the model.
     *
     * @param  string  $method
     * @param  array  $parameters
     * @return mixed
     */
    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);
        }

        // Model::create() を呼び出すと通る処理フローはこっち
        return $this->forwardCallTo($this->newQuery(), $method, $parameters);
    }

コードを見る限りリレーション以外の処理は最後の行で ForwardsCalls@forwardCallTo() に処理が移る (要調査)

forwardCallTo()

forwardCallTo() メソッドの実装は以下のようになっている

    /**
     * Forward a method call to the given object.
     *
     * @param  mixed  $object
     * @param  string  $method
     * @param  array  $parameters
     * @return mixed
     *
     * @throws \BadMethodCallException
     */
    protected function forwardCallTo($object, $method, $parameters)
    {
        try {
            return $object->{$method}(...$parameters);
        } catch (Error | BadMethodCallException $e) {
            ...(snip)...
        }
    }

実装の通り、forwardCallTo() は第 1 引数のオブジェクトに処理を委譲している

newQuery() の戻り値

結局 Model はどこに処理を委譲しているのか。これは newQuery() の戻り地を見るとわかる

処理フローはコードを見るとわかるが、いかんせん長いので結果だけをいうと最終的には newEloquentBuilder() メソッド内で Illuminate\Database\Eloquent\Builder クラスのインスタンスが生成され、これが返される

Illuminate\Database\Eloquent\Model クラス内

    /**
     * Get a new query builder for the model's table.
     *
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function newQuery()
    {
        return $this->registerGlobalScopes($this->newQueryWithoutScopes());
    }

    /**
     * Register the global scopes for this builder instance.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function registerGlobalScopes($builder)
    {
        foreach ($this->getGlobalScopes() as $identifier => $scope) {
            $builder->withGlobalScope($identifier, $scope);
        }

        return $builder;
    }

    /**
     * Get a new query builder that doesn't have any global scopes.
     *
     * @return \Illuminate\Database\Eloquent\Builder|static
     */
    public function newQueryWithoutScopes()
    {
        return $this->newModelQuery()
                    ->with($this->with)
                    ->withCount($this->withCount);
    }

    /**
     * Get a new query builder that doesn't have any global scopes or eager loading.
     *
     * @return \Illuminate\Database\Eloquent\Builder|static
     */
    public function newModelQuery()
    {
        return $this->newEloquentBuilder(
            $this->newBaseQueryBuilder()
        )->setModel($this);
    }

    /**
     * Create a new Eloquent query builder for the model.
     *
     * @param  \Illuminate\Database\Query\Builder  $query
     * @return \Illuminate\Database\Eloquent\Builder|static
     */
    public function newEloquentBuilder($query)
    {
        return new Builder($query);
    }

Illuminate\Database\Eloquent\Builder クラス内

    /**
     * Set a model instance for the model being queried.
     *
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @return $this
     */
    public function setModel(Model $model)
    {
        ...(snip)...
        return $this;
    }

おわりに

Eloquent Model は自身で実装していない処理は ForwardCalls トレイトを使って、Builder クラスに処理を委譲していることがわかった