Laravelのコードリーディングメモ。今日はいよいよEloquent。
まずは
$hoge = HogeModel::find();
がどうやって動いているのかを見ていきます。
まず Model::__callStatic() が呼ばれ、staticメソッドはインスタンスメソッドに置き換えて実行されます。
public static function __callStatic($method, $parameters)
{
return (new static)->$method(...$parameters);
}
インスタンスメソッドにも 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);
}
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);
}
first() は take(1) で LIMIT 1 して、 get() でEloquentCollectionを取得し、first() で最初のデータを返しています。
public function first($columns = ['*'])
{
return $this->take(1)->get($columns)->first();
}
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)
);
}
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;
}
こんな感じで生成されたEloquentModelではDBプロパティにアクセスしようとすると __get() が呼ばれ、設定した $attributes から値を取得しています。
public function __get($key)
{
return $this->getAttribute($key);
}