5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

リレーションはメソッドで定義したはずが、プロパティアクセスで動作する謎を解明する(Laravel)

Last updated at Posted at 2019-03-24

前提

Bookというモデルには、detail()というメソッドが定義してあるとする。この定義によりBookモデルとBookdetailモデルが、「bookdetail_idとid」を通して、1対1に結合(hasOne)していることになる。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
  public function detail(){
    return $this->hasOne('\App\Bookdetail','id','bookdetail_id');
  }
}

下記は、Bookモデルからid=1であるもののbookdetail_idを通して、Bookdetailモデルからisbnを出力するコードである。

class TestController extends Controller
{
  public function test(){
    $book = \App\Book::find(1);
    echo $book->detail->isbn;
  }
}

疑問点

先ほどのBookモデルでは、detail()とメソッドとして定義したはずだ。しかし、このコードでは、->detailとプロパティアクセスの形になっている。どこにもメソッドとして実行する記述はない。いったい何が起こっているのだろう?

Laravelのコアファイルを深掘りする

それでは、Laravelのコアファイルを一つずつ辿っていきたい。
本記事の動作前提となるファイルは下記に用意してあるので、自分でサンプルデータを作るのが面倒な方は、適宜利用いただきたい。
https://github.com/3tomcha/qiita

->detailとはつまり何をしているのか

まずはアクセスされる側の$bookの型を調べて見よう。dd()を使って出力するとBook型とわかる。(これは大方の予想通りであろう)
つまり->detailの正体を知るには、\App\Bookを調べるのが良さそうだと検討がつく。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TestController extends Controller
{
  public function test(){
    $book = \App\Book::find(1);
    dd($book);
    echo $book->detail->isbn;
  }
}

スクリーンショット 2019-03-24 12.09.19.png

さてApp\Bookをみてみると、直接関係ありそうな記述はない。よって、Illuminate\Database\Eloquent\Modelを辿ってみる。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
  public function detail(){
    return $this->hasOne('\App\Bookdetail','id','bookdetail_id');
  }
}

Illuminate\Database\Eloquent\Model をみると、気になる記述が見つかる。_____getはマジックメソッドと呼ばれるもので、アクセスできないプロパティにアクセスした時に呼ばれるものだ。これが働いているのではと検討がつく。実際にdump()を使ってみると、_____getは2回行われており、1回目はBookdetailオブジェクト、2回目は結果のisbnである"3333333333333"を返している。


    /**
     * Dynamically retrieve attributes on the model.
     *
     * @param  string  $key
     * @return mixed
     */
    public function __get($key)
    {
        return $this->getAttribute($key);
    }

スクリーンショット 2019-03-24 12.18.05.png

次に__getで行われている getAttribute()を辿って見よう。
Illuminate\Database\Eloquent\Concerns\HasAttrubute.php に定義されている。
今回は、hasOneというリレーションを定義しているため、最後の return $this->getRelationValue($key); が動いていそうだと検討を付けて、さらに進む。

    /**
     * Get an attribute from the model.
     *
     * @param  string  $key
     * @return mixed
     */
    public function getAttribute($key)
    {
        if (! $key) {
            return;
        }

        // If the attribute exists in the attribute array or has a "get" mutator we will
        // get the attribute's value. Otherwise, we will proceed as if the developers
        // are asking for a relationship's value. This covers both types of values.
        if (array_key_exists($key, $this->attributes) ||
            $this->hasGetMutator($key)) {
            return $this->getAttributeValue($key);
        }

        // Here we will determine if the model base class itself contains this given key
        // since we don't want to treat any of those methods as relationships because
        // they are all intended as helper methods and none of these are relations.
        if (method_exists(self::class, $key)) {
            return;
        }

        return $this->getRelationValue($key);
    }

getRelationValueは同じく Illuminate\Database\Eloquent\Concerns\HasAttrubute.php に定義されている。最下部にあるgetRelationshipFromMethod を辿って見よう。

/**
     * Get a relationship.
     *
     * @param  string  $key
     * @return mixed
     */
    public function getRelationValue($key)
    {
        // If the key already exists in the relationships array, it just means the
        // relationship has already been loaded, so we'll just return it out of
        // here because there is no need to query within the relations twice.
        if ($this->relationLoaded($key)) {
            return $this->relations[$key];
        }

        // If the "attribute" exists as a method on the model, we will just assume
        // it is a relationship and will load and return results from the query
        // and hydrate the relationship's value on the "relationships" array.
        if (method_exists($this, $key)) {
            return $this->getRelationshipFromMethod($key);
        }
    }

getRelationshipFromMethodは同じく Illuminate\Database\Eloquent\Concerns\HasAttrubute.php に定義されている。
ここで、今まで辿ってきたことに確信が持てるはずだ。$relation = $this->$method(); の記述によって、なぜ->detailとアクセスしたのに、detail()が動いているのだろうという疑問が解決された。

/**
     * Get a relationship value from a method.
     *
     * @param  string  $method
     * @return mixed
     *
     * @throws \LogicException
     */
    protected function getRelationshipFromMethod($method)
    {
        $relation = $this->$method();

        if (! $relation instanceof Relation) {
            throw new LogicException(sprintf(
                '%s::%s must return a relationship instance.', static::class, $method
            ));
        }

        return tap($relation->getResults(), function ($results) use ($method) {
            $this->setRelation($method, $results);
        });
    }

$methodと$relation をそれぞれdumpしてみると、予想通り$methodはdetailであり、$relationにはBookモデルのdetail()が行われた結果であるHasOneオブジェクトが入っていることがわかった。

スクリーンショット 2019-03-24 13.00.48.png

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
  public function detail(){
    return $this->hasOne('\App\Bookdetail','id','bookdetail_id');
  }
}

結論

プロパティアクセスの形でしか書いていないのに、モデルのメソッドが動く理由は、コアの処理の途中で$method()として実行しているため。

修正ご意見お待ちしています

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?