Laravel モデルwhere+カラム名メソッドの動きを理解しよう！

# はじめに
こんにちは！！
ミャンマー人エンジニアのピェッピョーアウンです。
私はミャンマーの大学を卒業して、新卒で日本の企業に就職し、日本での社会人として、5年目が終わり、6年目を向かいました。
エンジニアとして、LaravelとCodeIgniterといったPHP言語を中心にしたWeb開発が多かったのですがこれまでの日々の経験や勉強になったことなど投稿していこうと思っています。
今回はLaravel モデルでのwhere+カラム名メソッドについて、勉強になったことをシェアさせていただければと思います。

# where+カラム名メソッドとは

laravelのモデルでwhere+データベースのカラム名のメソッドを使ったことありませんか？
例えば、`User`モデルの場合、データベースの`users`テーブルには`name`と`email`カラムがあるとしたら、`whereName`、`whereEmail`のようなメソッドが定義されてなくても使えるようになっています。
以下、PHPDocで作成された、Userモデルを見てみましょう。

``` php:User.php
<?php
namespace App;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

/**
* App\User
*
* @property string $name
* @property string $password
* @property string $email
* @method static \Illuminate\Database\Query\Builder|\App\User whereName($value)
* @method static \Illuminate\Database\Query\Builder|\App\User wherePassword($value)
* @method static \Illuminate\Database\Query\Builder|\App\User whereEmail($value)
* @mixin \Eloquent
*/
class User extends Authenticatable
{
    use Notifiable;

    /**
    * The attributes that are mass assignable.
    *
    * @var array
    */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
    * The attributes that should be hidden for arrays.
    *
    * @var array
    */
    protected $hidden = [
        'password', 'remember_token',
    ];

    /**
    * The attributes that should be cast to native types.
    *
    * @var array
    */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}
```
ControllerとかServiceとかからはこのような使い方をしています。

``` php
$user = User::whereEmail('example@gmail.com');
```

ではなぜ、where+データベースカラム名の関数が使えるようになってるのでしょうか？
その前にPHPのマジックメソッド`__call()`と`__callStatic()`を理解していきましょう！

# PHPの`__call()`と`__callStatic()`マジックメソッドとは

マジックメソッドは、 ある動作がオブジェクトに対して行われた場合に、 PHP のデフォルトの動作を上書きする特別なメソッドです。

PHPではたくさんのマジックメソッドがあります。
その中、`__call()`は、 アクセス不能メソッドをオブジェクトのコンテキストで実行したときに起動します。`__callStatic()`マジックメソッドはアクセス不能メソッドをstaticメソッドとして呼び出した場合に起動します。


例えば
``` php
class MethodTest
{
    public function __call($name, $arguments)
    {
    echo "Calling object method '$name' ". implode(', ', $arguments). "\n";
    }

    public static function __callStatic($name, $arguments)
    {
    echo "Calling static method '$name' ". implode(', ', $arguments). "\n";
    }
}
$obj = new MethodTest();
$obj->runTest('in object context');
MethodTest::runTest('in static context');
```

を実行すると
```php
    Calling object method 'runTest' in object context
    Calling static method 'runTest' in static context
```

が出力されて、それぞれが呼び出されます。

# Laravel Eloquent の`__call()`と`__callStatic()`マジックメソッド

Laravelの`Illuminate\Database\Eloquent\Model`クラスに下記のようにマジックメソッドが定義されています。

```php
    /**
    * 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 $this->$method(...$parameters);
        }

        return $this->forwardCallTo($this->newQuery(), $method, $parameters);
    }

    /**
    * Handle dynamic static method calls into the method.
    *
    * @param  string  $method
    * @param  array  $parameters
    * @return mixed
    */
    public static function __callStatic($method, $parameters)
    {
        return (new static)->$method(...$parameters);
    }
```

なので、`User::whereEmail('example@gmail.com')`みたいにstaticメソッドとして呼び出したときに`__callStatic()`が起動され、それからオブジェクトコンテキストとして、また`__call()`が起動されます。

そして、`Illuminate\Support\Traits\ForwardsCalls` traitの`forwardCallTo()`メソッドにより、`Illuminate\Database\Eloquent\Builder`クラスのインスタンスのメソッドとして、メソッドの呼び出しが転送されます。
`Illuminate\Support\Traits\ForwardsCalls` traitの`forwardCallTo()`メソッドは以下のように定義されていて、`$object->{$method}(...$parameters);`で転送されます。

```php
    /**
     * Forward a method call to the given object.
     *
     * @param  mixed  $object
     * @param  string  $method
     * @param  array  $parameters
     * @return mixed
     *
     * @throws \BadMethodCallException
     */
    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);
        }
    }
```

それにより、`Illuminate\Database\Eloquent\Builder`クラスの`__call()`クラスが起動されます。

```php
    /**
     * Dynamically handle calls into the query instance.
     *
     * @param  string  $method
     * @param  array  $parameters
     * @return mixed
     */
    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->model !== null && method_exists($this->model, $scope = 'scope'.ucfirst($method))) {
            return $this->callScope([$this->model, $scope], $parameters);
        }

        if (in_array($method, $this->passthru)) {
            return $this->toBase()->{$method}(...$parameters);
        }

        $this->forwardCallTo($this->query, $method, $parameters);

        return $this;
    }

```

そちらにもたくさんの条件に満たされないので`forwardCallTo()`メソッドの`$this->forwardCallTo($this->query, $method, $parameters)`により、また`Illuminate\Database\Query\Builder`クラスの`__call()`メソッドに転送されます。

```php
    /**
     * Handle dynamic method calls into the method.
     *
     * @param  string  $method
     * @param  array  $parameters
     * @return mixed
     *
     * @throws \BadMethodCallException
     */
    public function __call($method, $parameters)
    {
        if (static::hasMacro($method)) {
            return $this->macroCall($method, $parameters);
        }

        if (Str::startsWith($method, 'where')) {
            return $this->dynamicWhere($method, $parameters);
        }

        static::throwBadMethodCallException($method);
    }

```

`Illuminate\Database\Query\Builder`クラスではメソッドの始まりに`where`文字がついていた場合、`$this->dynamicWhere($method, $parameters)`メソッドにより、動的に`where`条件が追加されるという仕組みとなっています。
※ 詳しくは`dynamicWhere`の中身を確認お願いします。

# 最後に
個人的には`User::where('email', '=', 'example@gmail.com')`の方が多く使いますが`User::whereEmail('example@gmail.com')`のメソッドを使っている方も珍しくはありません。
基本的には`=`でSQLのwhere句が追加されるようになっているのでこちらの方が分かりやすいという方はぜひ使ってみてください。この記事を読んでいただいて、皆さんの知識が一つでも増えると嬉しいです。