12
5

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-02-15

はじめに

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

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

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

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とかからはこのような使い方をしています。

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

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

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

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

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

例えば

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');

を実行すると

    Calling object method 'runTest' in object context
    Calling static method 'runTest' in static context

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

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

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

    /**
    * 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);で転送されます。

    /**
     * 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()クラスが起動されます。

    /**
     * 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()メソッドに転送されます。

    /**
     * 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句が追加されるようになっているのでこちらの方が分かりやすいという方はぜひ使ってみてください。この記事を読んでいただいて、皆さんの知識が一つでも増えると嬉しいです。

12
5
1

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
12
5