ローカルスコープ
前回の記事でローカルスコープについて書いたので、第2回の今回はローカルスコープのメソッドがどのように内部的に呼び出されるのかソースコードを読んでいこうと思います!
Modelクラスにこのようなローカルスコープメソッドを定義したとします。
class User extends Model
{
public function scopePopular($query)
{
return $query->where('votes', '>', 100);
}
}
そしてこのスコープを使用する時は
$users = User::query()
->popular()
->where('age', '>', 20)
->orderBy('created_at')
->get();
このように書きます。
今回はquery()
を挟んでいるので明示的にEloquentのBuilderクラスを返します。
なのでまず最初はEloquentのBuilderクラスにpopular()
というメソッドがないか探しに行きます。
ただこのメソッドは独自に作成したものなので当然Builderクラスには存在しません。
存在しない動的メソッドを呼び出した場合は__call()
が呼び出されます。
EloquentのBuilderクラスの__call()
はこのように定義されています。
public function __call($method, $parameters)
{
if ($method === 'macro') {
$this->localMacros[$parameters[0]] = $parameters[1];
return;
}
if ($this->hasMacro($method)) {
array_unshift($parameters, $this);
return $this->localMacros[$method](...$parameters);
}
if (static::hasGlobalMacro($method)) {
$callable = static::$macros[$method];
if ($callable instanceof Closure) {
$callable = $callable->bindTo($this, static::class);
}
return $callable(...$parameters);
}
if ($this->hasNamedScope($method)) {
return $this->callNamedScope($method, $parameters);
}
if (in_array($method, $this->passthru)) {
return $this->toBase()->{$method}(...$parameters);
}
$this->forwardCallTo($this->query, $method, $parameters);
return $this;
}
今回の場合は上から4つ目の下記のif文を通ります。
if ($this->hasNamedScope($method)) {
return $this->callNamedScope($method, $parameters);
}
hasNamedScopeメソッド
を見ていきます。
public function hasNamedScope($scope)
{
return $this->model && $this->model->hasNamedScope($scope);
}
次はModelクラスのhasNamedScopeメソッド
を見ます。
public function hasNamedScope($scope)
{
return method_exists($this, 'scope'.ucfirst($scope));
}
この$scope
の引数にはpopular
が渡されています。
ucfirst()
は先頭の文字を大文字にするメソッドです。なのでscopePopular
というメソッドが存在するかを確認しています。今回の場合このhasNamedScope
の返り値はtrue
になります。
そして処理を戻って、
if ($this->hasNamedScope($method))
このif文の中はtrueになり
return $this->callNamedScope($method, $parameters);
こちらが実行されます。
次はcallNamedScope
を見ていきます。
中身はこうなっています。
protected function callNamedScope($scope, array $parameters = [])
{
return $this->callScope(function (...$parameters) use ($scope) {
return $this->model->callNamedScope($scope, $parameters);
}, $parameters);
}
次はコールバックの中にあるModelクラスのcallNamedScope
を見ていきます。
public function callNamedScope($scope, array $parameters = [])
{
return $this->{'scope'.ucfirst($scope)}(...$parameters);
}
ありました!!!
ここでModelクラスで定義したscopePopular()
を返しているようです!
実際はこの部分はコールバックとしてcallScope()
内で実行されています。
protected function callScope(callable $scope, array $parameters = [])
{
array_unshift($parameters, $this);
$query = $this->getQuery();
$originalWhereCount = is_null($query->wheres)
? 0 : count($query->wheres);
$result = $scope(...$parameters) ?? $this;
if (count((array) $query->wheres) > $originalWhereCount) {
$this->addNewWheresWithinGroup($query, $originalWhereCount);
}
return $result;
}
if (count((array) $query->wheres) > $originalWhereCount) {
$this->addNewWheresWithinGroup($query, $originalWhereCount);
}
このif文では、スコープ内で実行されているクエリにwhere条件が含まれている場合にaddNewWheresWithinGroup
を実行しています。このメソッドでは改めて$wheresプロパティ
に条件を追加していく処理をしています。興味があればこの中身も見てみて下さい!
これがスコープの呼び出され方でした!!
我々が使用するときはpopular()
と呼び出すだけですが、内部ではこのような処理が走っていたんですね!
内部のコードを読んでいくと新たな発見があったりしてとても勉強になるのでおすすめです!
よかったらいいねして下さい!
最後まで読んでいただきありがとうございました。