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);
}