Laravelに備わっているデータベースをマッピングしたモデルであるEloquentには様々な便利な機能が備わっています。便利機能の一つにリレーションがあります。
Eloquent:リレーション 6.x Laravel
リレーションはあるEloquentに関係するEloquentをつなぐためのクエリビルダの準備メソッドともいえます。
// postsテーブルのモデル
class Post extends Model
{
/**
* ブログポストのコメントを取得
*/
public function comments()
{
return $this->hasMany('App\Comment');
}
}
// postsテーブル中のid=1のレコードとそれに関係するコメントを呼び出し
App\Post::find(1)->comments;
リレーションを記述するとこんな感じにプロパティの一部っぽく簡単に呼び出しを記述できます。ちなみに実際は次の様になっており、真のプロパティ、$attributesプロパティの中、真のメソッドの中、$relationsプロパティの中とたらい回しに探して呼び出されています。
/**
* インスタンス内に直に定義されていないプロパティを呼び出そうとするとこのメソッドが走ります。
* Get an attribute from the model.
*
* @param string $key
* @return mixed
*/
public function getAttribute($key)
{
if (! $key) {
return;
}
// 属性が属性配列に存在する場合、または「get」ミューテーターを持つ場合、属性の値を取得します。
// それ以外の場合は、開発者が関係の価値を求めているかのように進みます。
// これは両方のタイプの値をカバーします。
if (array_key_exists($key, $this->attributes) ||
// $attributesプロパティの中を探す
$this->hasGetMutator($key)) {
return $this->getAttributeValue($key);
}
// ここでは、すべてのメソッドはヘルパーメソッドであり、いずれもリレーションではないため、
// これらのメソッドをリレーションシップとして扱いたくないため、
// モデルの基本クラス自体にこのキーが含まれているかどうかを判断します。
// 実際に定義されているメソッドを探す
if (method_exists(self::class, $key)) {
return;
}
// $relationsプロパティの中を探す。あったらRelationクエリを発火させて値を確定させる
return $this->getRelationValue($key);
}
そんな感じでくせがありながらもプロパティ的に扱えて便利なリレーションです。時折リレーション先までまとめて処理(fill, saveなどなど)したくなる時があります(例えばトランザクションでくくった更新処理)。そういった時は次の様に再帰呼び出しを仕込むことで実現できます。
// 抽象
/**
* $this->hoge()を再帰させる。リレーション先のインスタンスがなくなり次第、再起終了
*/
public function recursiveHoge()
{
$this->hoge(); // 自身についてのhogeメソッドを実行
// インスタンス化済みのリレーション内を総ざらいする
foreach ($this->getRelations() as $relationName => $models) {
if ($models instanceof Collection) {
// リレーション先が多数の場合、リレーションしているインスタンスはIlluminate\Database\Eloquent\Collectionになっている
// Illuminate\Database\Eloquent\CollectionはIlluminate\Support\CollectionのEloquentについて拡張された継承クラス
foreach (array_filter($models) as $index => $model) {
$model->recursiveHoge(); // 再帰呼び出し
}
} else {
// リレーション先が単一の場合、モデルインスタンスがじかに置いてある
$models->recursiveHoge(); // 再帰呼び出し
}
}
}
// 具象
/**
* 読み込み済みのリレーション先まで再帰的にfillを実行する
* @param array $attributes
* @return $this
*/
public function recursiveFill(array $attributes): self
{
$this->fill($attributes);
foreach ($this->getRelations() as $relationName => $models) {
$currentAttributes = $attributes[$relationName] ?? $attributes[Str::snake($relationName)] ?? null;
if ($currentAttributes === null) {
continue;
}
if ($models instanceof Collection) {
foreach (array_filter($models) as $index => $model) {
$model->recursiveFill($currentAttributes[$index]);
}
} else {
$models->recursiveFill($currentAttributes);
}
}
return $this;
}