2024-07-24

LaravelのEloquentの仕組み: Model::find()

Laravelのコードリーディングメモ。今日はいよいよEloquent。

まずは

$hoge = HogeModel::find();

がどうやって動いているのかを見ていきます。


まず Model::__callStatic() が呼ばれ、staticメソッドはインスタンスメソッドに置き換えて実行されます。

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

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Database/Eloquent/Model.php#L2349-L2352

インスタンスメソッドにも find() メソッドは存在しないので __call() が呼ばれます。

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

    if ($resolver = $this->relationResolver(static::class, $method)) {
        return $resolver($this);
    }

    if (Str::startsWith($method, 'through') &&
        method_exists($this, $relationMethod = Str::of($method)->after('through')->lcfirst()->toString())) {
        return $this->through($relationMethod);
    }

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

最終的に $this->newQuery() に処理が委譲されます。

newQuery() は対象のモデルを setModel() でセットした Illuminate\Database\Eloquent\Builder のインスタンスを生成します。

public function newQuery()
{
    return $this->registerGlobalScopes($this->newQueryWithoutScopes());
}

public function newQueryWithoutScopes()
{
    return $this->newModelQuery()
        ->with($this->with)
        ->withCount($this->withCount);
}

public function newModelQuery()
{
    return $this->newEloquentBuilder(
        $this->newBaseQueryBuilder()
    )->setModel($this);
}

public function newEloquentBuilder($query)
{
    return new Builder($query);
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Database/Eloquent/Model.php#L1487-L1490 https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Database/Eloquent/Model.php#L1534-L1539 https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Database/Eloquent/Model.php#L1497-L1502 https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Database/Eloquent/Model.php#L1569-L1572 https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Database/Eloquent/Builder.php#L1837-L1844

forwardCallTo()$object$method$parameters パラメータを付けて呼び出す委譲メソッドです。ということで今回はEloquentBuilderの find() が呼ばれます。

protected function forwardCallTo($object, $method, $parameters)
{
    try {
        return $object->{$method}(...$parameters);
    } catch (Error|BadMethodCallException $e) {
        $pattern = '~^Call to undefined method (?P<class>[^:]+)::(?P<method>[^\(]+)\(\)$~';

        if (! preg_match($pattern, $e->getMessage(), $matches)) {
            throw $e;
        }

        if ($matches['class'] != get_class($object) ||
            $matches['method'] != $method) {
            throw $e;
        }

        static::throwBadMethodCallException($method);
    }
}

引数がarrayじゃない場合は whereKey() で絞り込みをして first() でデータを取得します。

public function find($id, $columns = ['*'])
{
    if (is_array($id) || $id instanceof Arrayable) {
        return $this->findMany($id, $columns);
    }

    return $this->whereKey($id)->first($columns);
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Database/Eloquent/Builder.php#L442-L449

whereKey()WHERE {主キー} = {id} となるような条件を作成します。

public function whereKey($id)
{
    if ($id instanceof Model) {
        $id = $id->getKey();
    }

    if (is_array($id) || $id instanceof Arrayable) {
        if (in_array($this->model->getKeyType(), ['int', 'integer'])) {
            $this->query->whereIntegerInRaw($this->model->getQualifiedKeyName(), $id);
        } else {
            $this->query->whereIn($this->model->getQualifiedKeyName(), $id);
        }

        return $this;
    }

    if ($id !== null && $this->model->getKeyType() === 'string') {
        $id = (string) $id;
    }

    return $this->where($this->model->getQualifiedKeyName(), '=', $id);
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Database/Eloquent/Builder.php#L234-L255

first()take(1)LIMIT 1 して、 get() でEloquentCollectionを取得し、first() で最初のデータを返しています。

public function first($columns = ['*'])
{
    return $this->take(1)->get($columns)->first();
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Database/Concerns/BuildsQueries.php#L331-L334

get()getModels() でModelの配列を取得し、 newCollection() でEloquentCollectionを生成します。

public function get($columns = ['*'])
{
    $builder = $this->applyScopes();

    // If we actually found models we will also eager load any relationships that
    // have been specified as needing to be eager loaded, which will solve the
    // n+1 query issue for the developers to avoid running a lot of queries.
    if (count($models = $builder->getModels($columns)) > 0) {
        $models = $builder->eagerLoadRelations($models);
    }

    return $this->applyAfterQueryCallbacks(
        $builder->getModel()->newCollection($models)
    );
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Database/Eloquent/Builder.php#L715-L727

getModels()\Illuminate\Database\Query\Builder::get()->all() でstdClassな配列を生成し、Model::hydrate() でstdClassからモデルに変換しています。

public function getModels($columns = ['*'])
{
    return $this->model->hydrate(
        $this->query->get($columns)->all()
    )->all();
}

public function hydrate(array $items)
{
    $instance = $this->newModelInstance();

    return $instance->newCollection(array_map(function ($item) use ($items, $instance) {
        $model = $instance->newFromBuilder($item);

        if (count($items) > 1) {
            $model->preventsLazyLoading = Model::preventsLazyLoading();
        }

        return $model;
    }, $items));
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Database/Eloquent/Builder.php#L735-L740 https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Database/Eloquent/Builder.php#L406-L419

newFromBuilder() ではモデルのインスタンスを生成して $attributes を設定しています。

public function newFromBuilder($attributes = [], $connection = null)
{
    $model = $this->newInstance([], true);

    $model->setRawAttributes((array) $attributes, true);

    $model->setConnection($connection ?: $this->getConnectionName());

    $model->fireModelEvent('retrieved', false);

    return $model;
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Database/Eloquent/Model.php#L626-L637

こんな感じで生成されたEloquentModelではDBプロパティにアクセスしようとすると __get() が呼ばれ、設定した $attributes から値を取得しています。

public function __get($key)
{
    return $this->getAttribute($key);
}

https://github.com/laravel/framework/blob/v11.0.7/src/Illuminate/Database/Eloquent/Model.php#L2229-L2232

このエントリーをはてなブックマークに追加