LoginSignup
5
0

More than 5 years have passed since last update.

PHPStormで追いにくいEloquentコードリーディング(このメソッドの実体はどこにあるの!?)

Last updated at Posted at 2018-12-19

これは何

Laravel歴4ヶ月の初心者がEloquent周りのコードを読んで頑張って解釈した記録です。
【Laravel】 第1回 Eloquent ソースコードリーディング - モデルの取得 を参考にしました。詳しく解説していただけてますが、初心者には追うのが辛かった部分もあるので、難しそうなところをすっ飛ばしてタイプヒンティングに頼って最終的に呼ばれるメソッドがどれなのか?というのを見つけようという目的です。
例えばorderBy()とかget()とかModelに直接生えていないけど、呼ぶとなんか使えるけどなんで???という疑問を解決したいです。

バージョンとか

Laravel v5.6 です。最近5.7にアップデートするタスクをやったのですが、まだ手元のコードが5.6なのでそれをそのまま使います。そんなに構造は大きく変わらないだろうと思ってます。

読み解く対象のコード

EloquentModel を extend したNewsModel の動きを見ていきたいと思います。

class NewsModel extends Illuminate\Database\Eloquent\Model

NewsRepositoyはこのModelを使用するとします。

NewsRepository.php
    public function getList()
    {
        $collection = NewsModel::orderBy('id', 'desc')->get();

        // 色々処理をしてreturnする的なところは省略
    }

ここで疑問がいくつかあって
* orderBy('id', 'desc') はどこのクラスに生えてて、何を返しているのか?
* get() は同様に何を返しているのか?
* なんでこの二つはメソッドチェインできるの?

なぜこの疑問が出てくるかというとPHPStormを使って定義へジャンプできないからです。普段書いているコードだと大体ジャンプできるのに、なんでここはジャンプできないんだ?という初心者からすると不思議な動きに感じました。

まず、Modelのクラスの中にstaticメソッドとして定義されているのか? というところですが、なさそうです。
さて、この疑問を解決するかぎですが”マジックメソッド”です。
公式ドキュメント によると

__call() は、 アクセス不能メソッドをオブジェクトのコンテキストで実行したときに起動します。
__callStatic() は、 アクセス不能メソッドを静的コンテキストで実行したときに起動します。

ざっくりいうと、指定したメソッドが存在しなかったらstaticメソッドの呼び出し方(class::methodの形)だったら __callStatic() の処理を呼び、インスタンスメソッドなら(class->methodの形だったら) __call() の処理を呼ぶと解釈できそうです。

で、話を戻すと、orderByはNewsModel::orderBy('id', 'desc')の形で呼ばれているので__callStaticの方が呼ばれそうです。

Illuminate/Database/Eloquent/Model.php
    public static function __callStatic($method, $parameters)
    {
        return (new static)->$method(...$parameters);
    }

(new static) は自分自身をインスタンス化するもので、つまり NewsModel->orderBy を呼んだ場合と同じになっています
参考: staticメソッド内でサブクラス自身を表す際にはselfではなくstatic

で、staticなクラスメソッドもなかったのですが、今度はインスタンスメソッドもなさそうなので、今度は __call が呼ばれます。

Illuminate/Database/Eloquent/Model.php
    public function __call($method, $parameters)
    {
        // メソッド名がincrement, decrementではないのでここには入らないです
        if (in_array($method, ['increment', 'decrement'])) {
            return $this->$method(...$parameters);
        }

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

ここでは $this->newQuery()->$method(...$parameters) が呼ばれており、じゃあ newQuery() はなんなのか?ということになります
これは同クラスのなかに定義されており以下のようになっていました。

Illuminate/Database/Eloquent/Model.php
    public function newQuery()
    {
        return $this->registerGlobalScopes($this->newQueryWithoutScopes());
    }

さっぱりわかりませんね。しかし、むやみにコードを読むことはせず、registerGlobalScopesにいき、アノーテーションを読んでみると以下のようになっています。

Illuminate/Database/Eloquent/Model.php
    /**
     * Register the global scopes for this builder instance.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function registerGlobalScopes($builder)

要するに、Builderが帰ってくることが決められています。戻り値がmixedとかだと困ってしまうのですが、こういう風に明確に戻り値がある場合には甘えてしまいましょう。

さあ orderBy はこのBuilderクラスにあるのでしょうか? 残念ながらなさそうです。
ではまた __call メソッドを追ってみましょう。

以下のような処理になっていますが、まともに読むと辛そうです。しかし、macroなんて定義していないので、最後の $this->query->{$method}(...$parameters); に処理が入るのでは?と期待して読み進めてみたいと思います。

Illuminate/Database/Eloquent/Model.php
    public function __call($method, $parameters)
    {
        if ($method === 'macro') {
            $this->localMacros[$parameters[0]] = $parameters[1];

            return;
        }

        if (isset($this->localMacros[$method])) {
            array_unshift($parameters, $this);

            return $this->localMacros[$method](...$parameters);
        }

        if (isset(static::$macros[$method])) {
            if (static::$macros[$method] instanceof Closure) {
                return call_user_func_array(static::$macros[$method]->bindTo($this, static::class), $parameters);
            }

            return call_user_func_array(static::$macros[$method], $parameters);
        }

        if (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->query->{$method}(...$parameters);

        return $this;
    }

さて、 $this->query に何が入っているかというと、また丁寧に教えてくれました(同じBuilder.phpだけど違うところのやつなので注意)

Illuminate/Database/Eloquent/Builder.php

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

指定のQuery/Builderクラスの中を探してみると、、、 ありました!!!!

Illuminate\Database\Query\Builder.php
    /**
     * Add an "order by" clause to the query.
     *
     * @param  string  $column
     * @param  string  $direction
     * @return $this
     */
    public function orderBy($column, $direction = 'asc')
    {
        $this->{$this->unions ? 'unionOrders' : 'orders'}[] = [
            'column' => $column,
            'direction' => strtolower($direction) == 'asc' ? 'asc' : 'desc',
        ];

        return $this;
    }

やっとたどり着いたのでした。
で、return $thisということは Illuminate\Database\Query\Builder.php がまた帰ってきているので、そこからチェインして同じ関数にある関数が呼べるということです。
なので、get()は同じクラスにあることが予想されます。ということで、探してみるとありました。

Illuminate\Database\Query\Builder.php
    /**
     * Execute the query as a "select" statement.
     *
     * @param  array  $columns
     * @return \Illuminate\Support\Collection
     */
    public function get($columns = ['*'])
    {
        return collect($this->onceWithColumns($columns, function () {
            return $this->processor->processSelect($this, $this->runSelect());
        }));
    }

get()の戻り値として Collection が指定されているので、get()により得られたものはCollectionに生えている便利メソッドが色々使えるということになります。
コレクションについての公式ドキュメント

終わりに

無事に追うことができたので、何が戻り値として帰ってきて、次にどのメソッドがチェインできるのかというのを自分で考えることができるようになりました。
もうちょっと読むのに慣れてきたら階層構造とか意識して、もっと本質的な理解を目指したいと思います。

5
0
0

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