😶

Model::whereで何が起きているのかコードリーディングしてみた

2023/12/18に公開

こんにちは!くすみです。
SDB Tech Blog Advent Calendar 2023の 18 日目です。

普段、開発をしているとこれってなんで動くんだろう? とか 何故か上手くいってしまった なんていう機会が少なくはないんじゃないでしょうか?

私はよくあります。
そんな時はそういうものなんだと思って次のタスクに着手すれば良いわけなんですが、せっかく気になったなら仕組みを理解してみたいですよね。

ということで、本記事では最近私が気になってコードリーディングした Model::where()メソッドについてまとめていきたいと思います!

Model::where では何が行われているのか

私は普段、PHP, Laravel を使用して開発をしています。
PHP における Web 開発、特に Laravel フレームワークを使用する際、Model::where()メソッドはデータベースのクエリ(フィルタリング)を簡単に作成できるので頻繁に使用されます。

User::where('age', '>=', '20')

例えば、上記のコードは 20 歳以上の全てのユーザーを取得してくれます。

ちなみに、このコードは Laravel の Eloquent ORM を使用しています。

Laravel には、データベースとの対話を楽しくするオブジェクトリレーショナルマッパー(ORM)である Eloquent が含まれています。Eloquent を使用する場合、各データベーステーブルには対応する「モデル」があり、そのテーブルとの対話に使用します。
https://readouble.com/laravel/8.x/ja/eloquent.html

ORM(Object-Relational Mapping)は、オブジェクト指向と RDB の橋渡し的な存在で、プログラム内のオブジェクトと、データベース内のテーブルを結びつけるツールです。
Laravel では Eloquent という ORM が提供されており、複雑なデータベース操作を簡単なコードで行えます。

この Eloquent を使用したシンプルなメソッドの背後には、Laravel によって提供されている、非常に便利なロジックが使用されています。
本記事では、Model::where()がどのように動作しているのかについて掘り下げたいと思います!

Model クラスに where()メソッドは存在するの?

まず始めに、Model クラスのwhere()メソッドを確認しに行きましょう。

以下は、Laravel8.x の Model クラスのコードです。(Zenn に表示されるのは、200 行までなので是非 github に飛んで、確認してみてください)

https://github.com/laravel/framework/blob/8.x/src/Illuminate/Database/Eloquent/Model.php

いかがでしたしょうか。
このようなコードは見つかりましたか?

public function where($column, $operator = null, $value = null, $boolean = 'and')
{

}

恐らく見つからなかったと思います。
Model::where()メソッドの動きを調べるのにメソッドが存在しないの!?と思われるかもしれませんが、Model クラスに where()メソッドは存在しません。
では、なぜ最初のコードは動作するのでしょうか?

本記事のテーマである
where()メソッドが存在しないのにどうして動いてくれるのか?
について実際にコードを追って調べてみましょう!

コードリーディング

メソッドの呼び出し

メソッドの呼び出しには本記事の最初に挙げた例を使用します。

User::where('age', '>=', '20')

__callStatic()(マジックメソッド)の利用

既に直接調べていただいたように Model クラスに where()メソッドは存在しません。
存在しないメソッドが呼ばれると、PHP は Model クラスの__callStatic() というマジックメソッドを呼び出します。

__callStatic() は、 アクセス不能メソッドを static メソッドとして呼び出した場合に起動します。
https://www.php.net/manual/ja/language.oop5.overloading.php#object.callstatic

//Illuminate/Database/Eloquent/Model.php

//以下は引数の詳細
//$method = 'where';
//$parameters = ['age', '>=', '20'];
public static function __callStatic($method, $parameters)
{
    return (new static)->$method(...$parameters);
}

__callStatic() では、Model のインスタンスを生成します。
その後、where()メソッド を実行しようとするがこれは定義されていないので__call() に処理が移ります。

__call()(マジックメソッド)の利用

//Illuminate/Database/Eloquent/Model.php

//$method = 'where';
//$parameters = ['age', '>=', '20'];
public function __call($method, $parameters)
{
  if (in_array($method, ['increment', 'decrement'])) {
      return $this->$method(...$parameters);
  }

  if ($resolver = (static::$relationResolvers[get_class($this)][$method] ?? null)) {
      return $resolver($this);
  }

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

__call() では以下の箇所が採用されます。

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

この行では Eloquent モデルクラスで直接サポートされていないメソッドの呼び出し(例:where, orderBy など)を、クエリビルダインスタンスに委譲し、そこで実行させます。

Builder の生成

//Illuminate/Database/Eloquent/Model.php

/**
* Get a new query builder for the model's table.
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function newQuery()
{
  return $this->registerGlobalScopes($this->newQueryWithoutScopes());
}

newQuery() メソッドでは新しいクエリビルダインスタンスを生成し、forwardCallTo()でクエリビルダに対してメソッドの呼び出しを委譲が可能です。

これにより、Eloquent モデルはクエリビルダのメソッドを呼び出すことができるようになります。

Eloquent の where()メソッド

//Illuminate/Database/Eloquent/Builder.php

public function where($column, $operator = null, $value = null, $boolean = 'and')
{
  if ($column instanceof Closure && is_null($operator)) {
      $column($query = $this->model->newQueryWithoutRelationships());

      $this->query->addNestedWhereQuery($query->getQuery(), $boolean);
  } else {
      $this->query->where(...func_get_args());
  }

  return $this;
}

場所は変わって Eloquent/Builder クラスに移動しました。
Builder クラスには where()メソッドが定義されており、このメソッドはクエリの条件を構築してくれます。
ここで条件(今回では'age', '>=', '20')がクエリに追加されます

クエリビルダの where()メソッド

//Illuminate/Database/Query/Builder.php

public function where($column, $operator = null, $value = null, $boolean = 'and')
{
  //(省略)

  $this->wheres[] = compact(
      'type', 'column', 'operator', 'value', 'boolean'
  );

  if (! $value instanceof Expression) {
      $this->addBinding($this->flattenValue($value), 'where');
  }

  return $this;
}

ここではクエリ構築のための条件を作成しています。
実際に作られた条件はこちらです。

type: 'Basic'
column: 'age'
operator: '>='
value: '20'
boolean: 'and'(デフォルト値)

このような流れを経て、20 歳以上のユーザーを取得できるようになります。

Model クラスで呼び出されたはずの where()メソッドは気付けば Eloquent\Builder クラスにある where()メソッドへ委譲、実行されることがわかりました。

まとめ

実際に調べてみると簡単そうなメソッド 1 つでもその背景には多くのコードが存在していて、コードリーディングにかなりの労力を費やしました...。

しかし、PHP や Laravel を使用する開発者として、今回のように内部動作を理解することは綺麗で読みやすいコードを書くためにも必要不可欠ですよね。
特に開発における中級~上級者になるためには必須なことだと思います。

今回のコードリーディングをきっかけに今後も PHP, Laravel のコードを調べてみたいと思います。
皆さんも是非気になったメソッドがあれば調べてみてください!

ソーシャルデータバンク テックブログ

Discussion