前提
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;
}
}
さて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);
}
次に__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オブジェクトが入っていることがわかった。
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Book extends Model
{
public function detail(){
return $this->hasOne('\App\Bookdetail','id','bookdetail_id');
}
}
結論
プロパティアクセスの形でしか書いていないのに、モデルのメソッドが動く理由は、コアの処理の途中で$method()として実行しているため。
修正ご意見お待ちしています