背景
Laravel PHP , Laravel

【Laravel5】Eloquent ORMと2つのBuilderクラス

こんにちは、ナカエです。
 

Laravel標準のORMであるEloquentはマジックメソッドによるメソッドの委譲が利用されており、


Article::where('title', 'LIKE', "%タイトル%")
       ->where('category_id', '=', 1)
       ->orderBy('created_at', 'desc')
       ->get();
 

という優雅なコードの裏ではModelクラス以外にも2つのBuilderクラスが働いています。

この委譲関係を把握することはEloquentの理解にとって非常に有意義です。

※下記はLaravel 5.1 LTSのコードを参照しています。

\Illuminate\Database\Eloquent\Builderクラス

フルネームが長いので以下Eloquentビルダーと呼称します。

where, findなどModelクラスにstaticメソッドが存在しない場合は Eloquentビルダークラスのインスタンスに委譲されます


    /**
     * 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 call_user_func_array([$this, $method], $parameters);
        }

        $query = $this->newQuery();

        return call_user_func_array([$query, $method], $parameters);
    }
 

マジックメソッドの_call()により、Model::newQuery()で生成した Eloquentビルダーのインスタンスの同名のメソッドが呼ばれます。


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

        return $this->applyGlobalScopes($builder);
    }
 

※Model::newQuery()では、Modelに定義したグローバルスコープがあらかじめ適用されます。

Modelクラスにはstaticメソッドのquery()が定義されており、 明示的にEloquentビルダーを呼ぶことも可能です。

・\Illuminate\Database\Eloquent\Model


    /**
     * Begin querying the model.
     *
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public static function query()
    {
        return (new static)->newQuery();
    }
 

例えば

$article = Article::find(1);
$article = Article::query()->find(1);
 

は同じ結果になります。後者は記述が少し長くなりますがIDEで補完が利きやすいです。

\Illuminate\Database\Query\Builderクラス

例によって長いのでQueryビルダーと呼称します。

orderBy()などEloquentビルダーのクラスに存在しないメソッドは さらにQueryビルダーに委譲されます。

EloquentビルダーはQueryビルダーのインスタンスを内部に保持しています。

実のところWhere条件などクエリの実体に対応する変数群はQueryビルダーが持っており、 Eloquentビルダーはクエリの返り値をModelやModelのCollectionとして利用するためにQueryビルダーをラップしたクラスです。

・\Illuminate\Database\Eloquent\Builder


    /**
     * The base query builder instance.
     *
     * @var \Illuminate\Database\Query\Builder
     */
    protected $query;

    (略)

    /**
     * Create a new Eloquent query builder instance.
     *
     * @param  \Illuminate\Database\Query\Builder  $query
     * @return void
     */
    public function __construct(QueryBuilder $query)
    {
        $this->query = $query;
    }
 

Eloquentビルダーに存在しないメソッドは

  • Eloquentビルダーに登録されたマクロ
  • モデルで定義されたQuery Scope
  • Queryビルダーの同名メソッド

の優先順位で探索されます。


    /**
     * Dynamically handle calls into the query instance.
     *
     * @param  string  $method
     * @param  array   $parameters
     * @return mixed
     */
    public function __call($method, $parameters)
    {
        if (isset($this->macros[$method])) {
            array_unshift($parameters, $this);

            return call_user_func_array($this->macros[$method], $parameters);
        } elseif (method_exists($this->model, $scope = 'scope'.ucfirst($method))) {
            return $this->callScope($scope, $parameters);
        }

        $result = call_user_func_array([$this->query, $method], $parameters);

        return in_array($method, $this->passthru) ? $result : $this;
    }
 

Queryビルダーの同名メソッドが呼ばれる場合、 __call()の返り値はメソッドチェーンを実現するためにEloquentビルダーそのものになる場合が多いです。
一部のメソッドはQueryビルダーの返り値をそのまま返します。


    /**
     * The methods that should be returned from query builder.
     *
     * @var array
     */
    protected $passthru = [
        'insert', 'insertGetId', 'getBindings', 'toSql',
        'exists', 'count', 'min', 'max', 'avg', 'sum',
    ];
 

まとめ

  • find()などModelクラスに定義されていないメソッドはEloquentビルダーに委譲される
  • orderBy()などEloqunetビルダークラスに定義されていないメソッドはQueryビルダーに委譲される
  • EloquentビルダーはQueryビルダーを内部に保持しており、where条件などはQueryビルダーが持っている
  • どちらのビルダーが利用されているか意識するとIDEの恩恵を受けやすいコードが書けるかも?
≪ 【Laravel】Paginatorのラッパークラス  |  【PHP】変数値を元にプロパティやメソッドにアクセスする ≫

Web制作のお問い合わせ

075-744-6842

(平日/土曜 10:00~17:00)

 お問い合わせ