16
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Laravelのソースコードリーディング第2回

Last updated at Posted at 2023-02-10

ローカルスコープ

前回の記事でローカルスコープについて書いたので、第2回の今回はローカルスコープのメソッドがどのように内部的に呼び出されるのかソースコードを読んでいこうと思います!

Modelクラスにこのようなローカルスコープメソッドを定義したとします。

User.php
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()はこのように定義されています。

Eloquent/Builder.php
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メソッドを見ます。

Model.php
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を見ていきます。

Model.php
public function callNamedScope($scope, array $parameters = [])
{
    return $this->{'scope'.ucfirst($scope)}(...$parameters);
}

ありました!!!
ここでModelクラスで定義したscopePopular()を返しているようです!

実際はこの部分はコールバックとしてcallScope()内で実行されています。

Builder.php
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()と呼び出すだけですが、内部ではこのような処理が走っていたんですね!
内部のコードを読んでいくと新たな発見があったりしてとても勉強になるのでおすすめです!

よかったらいいねして下さい!

最後まで読んでいただきありがとうございました。

16
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?