【Laravel5】Eloquent ORMと2つのBuilderクラス
こんにちは、ナカエです。
Laravel標準のORMであるEloquentはマジックメソッドによるメソッドの委譲が利用されており、
Article::where('title', 'LIKE', "%タイトル%")
->where('category_id', '=', 1)
->orderBy('created_at', 'desc')
->get();
という優雅なコードの裏ではModelクラス以外にも2つのBuilderクラスが働いています。
この委譲関係を把握することはEloquentの理解にとって非常に有意義です。
※下記はLaravel 5.1 LTSのコードを参照しています。
\Illuminate\Database\Eloquent\Builderクラス
フルネームが長いので以下Eloquentビルダーと呼称します。
where, findなどModelクラスにstaticメソッドが存在しない場合は Eloquentビルダークラスのインスタンスに委譲されます
/**
* 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 call_user_func_array([$this, $method], $parameters);
}
$query = $this->newQuery();
return call_user_func_array([$query, $method], $parameters);
}
マジックメソッドの_call()により、Model::newQuery()で生成した Eloquentビルダーのインスタンスの同名のメソッドが呼ばれます。
/**
* Get a new query builder for the model's table.
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function newQuery()
{
$builder = $this->newQueryWithoutScopes();
return $this->applyGlobalScopes($builder);
}
※Model::newQuery()では、Modelに定義したグローバルスコープがあらかじめ適用されます。
Modelクラスにはstaticメソッドのquery()が定義されており、 明示的にEloquentビルダーを呼ぶことも可能です。
・\Illuminate\Database\Eloquent\Model
/**
* Begin querying the model.
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public static function query()
{
return (new static)->newQuery();
}
例えば
$article = Article::find(1);
$article = Article::query()->find(1);
は同じ結果になります。後者は記述が少し長くなりますがIDEで補完が利きやすいです。
\Illuminate\Database\Query\Builderクラス
例によって長いのでQueryビルダーと呼称します。
orderBy()などEloquentビルダーのクラスに存在しないメソッドは さらにQueryビルダーに委譲されます。
EloquentビルダーはQueryビルダーのインスタンスを内部に保持しています。
実のところWhere条件などクエリの実体に対応する変数群はQueryビルダーが持っており、 Eloquentビルダーはクエリの返り値をModelやModelのCollectionとして利用するためにQueryビルダーをラップしたクラスです。
・\Illuminate\Database\Eloquent\Builder
/**
* The base query builder instance.
*
* @var \Illuminate\Database\Query\Builder
*/
protected $query;
(略)
/**
* Create a new Eloquent query builder instance.
*
* @param \Illuminate\Database\Query\Builder $query
* @return void
*/
public function __construct(QueryBuilder $query)
{
$this->query = $query;
}
Eloquentビルダーに存在しないメソッドは
- Eloquentビルダーに登録されたマクロ
- モデルで定義されたQuery Scope
- Queryビルダーの同名メソッド
の優先順位で探索されます。
/**
* Dynamically handle calls into the query instance.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
if (isset($this->macros[$method])) {
array_unshift($parameters, $this);
return call_user_func_array($this->macros[$method], $parameters);
} elseif (method_exists($this->model, $scope = 'scope'.ucfirst($method))) {
return $this->callScope($scope, $parameters);
}
$result = call_user_func_array([$this->query, $method], $parameters);
return in_array($method, $this->passthru) ? $result : $this;
}
Queryビルダーの同名メソッドが呼ばれる場合、 __call()の返り値はメソッドチェーンを実現するためにEloquentビルダーそのものになる場合が多いです。
一部のメソッドはQueryビルダーの返り値をそのまま返します。
/**
* The methods that should be returned from query builder.
*
* @var array
*/
protected $passthru = [
'insert', 'insertGetId', 'getBindings', 'toSql',
'exists', 'count', 'min', 'max', 'avg', 'sum',
];
まとめ
- find()などModelクラスに定義されていないメソッドはEloquentビルダーに委譲される
- orderBy()などEloqunetビルダークラスに定義されていないメソッドはQueryビルダーに委譲される
- EloquentビルダーはQueryビルダーを内部に保持しており、where条件などはQueryビルダーが持っている
- どちらのビルダーが利用されているか意識するとIDEの恩恵を受けやすいコードが書けるかも?