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

## ローカルスコープ
[前回の記事](https://qiita.com/ryocha12/items/201c1cc51a53c5da7af8)でローカルスコープについて書いたので、第2回の今回はローカルスコープのメソッドがどのように内部的に呼び出されるのかソースコードを読んでいこうと思います！

Modelクラスにこのようなローカルスコープメソッドを定義したとします。
```php:User.php
class User extends Model
{
    public function scopePopular($query)
    {
        return $query->where('votes', '>', 100);
    }
}
```

そしてこのスコープを使用する時は
```php
$users = User::query()
    ->popular()
    ->where('age', '>', 20)
    ->orderBy('created_at')
    ->get();
```
このように書きます。
今回は`query()`を挟んでいるので明示的にEloquentのBuilderクラスを返します。
なのでまず最初はEloquentのBuilderクラスに`popular()`というメソッドがないか探しに行きます。
ただこのメソッドは独自に作成したものなので当然Builderクラスには存在しません。
存在しない動的メソッドを呼び出した場合は`__call()`が呼び出されます。
EloquentのBuilderクラスの`__call()`はこのように定義されています。
```php: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文を通ります。
```php
if ($this->hasNamedScope($method)) {
    return $this->callNamedScope($method, $parameters);
}
```
`hasNamedScopeメソッド`を見ていきます。
```php
public function hasNamedScope($scope)
{
    return $this->model && $this->model->hasNamedScope($scope);
}
```
次はModelクラスの`hasNamedScopeメソッド`を見ます。
```php:Model.php
public function hasNamedScope($scope)
{
    return method_exists($this, 'scope'.ucfirst($scope));
}
```
この`$scope`の引数には`popular`が渡されています。
`ucfirst()`は先頭の文字を大文字にするメソッドです。なので`scopePopular`というメソッドが存在するかを確認しています。今回の場合この`hasNamedScope`の返り値は`true`になります。

そして処理を戻って、
```php
if ($this->hasNamedScope($method))
```
このif文の中はtrueになり
```php
return $this->callNamedScope($method, $parameters);
```
こちらが実行されます。

次は`callNamedScope`を見ていきます。
中身はこうなっています。
```php
protected function callNamedScope($scope, array $parameters = [])
{
    return $this->callScope(function (...$parameters) use ($scope) {
        return $this->model->callNamedScope($scope, $parameters);
    }, $parameters);
}
```
次はコールバックの中にあるModelクラスの`callNamedScope`を見ていきます。
```php:Model.php
public function callNamedScope($scope, array $parameters = [])
{
    return $this->{'scope'.ucfirst($scope)}(...$parameters);
}
```
ありました！！！
ここでModelクラスで定義した`scopePopular()`を返しているようです！

実際はこの部分はコールバックとして`callScope()`内で実行されています。
```php: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;
}
```
```php
if (count((array) $query->wheres) > $originalWhereCount) {
    $this->addNewWheresWithinGroup($query, $originalWhereCount);
}
```
このif文では、スコープ内で実行されているクエリにwhere条件が含まれている場合に`addNewWheresWithinGroup`を実行しています。このメソッドでは改めて`$wheresプロパティ`に条件を追加していく処理をしています。興味があればこの中身も見てみて下さい！

これがスコープの呼び出され方でした！！

我々が使用するときは`popular()`と呼び出すだけですが、内部ではこのような処理が走っていたんですね！
内部のコードを読んでいくと新たな発見があったりしてとても勉強になるのでおすすめです！

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

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