Laravel には多対多を表現するHasManyThroughという仕組みがあり、これを利用して中間テーブルのレコードを他の具体的なモデル名を参照しなくとも操作できます。具体的には、次のメソッドをEloquentモデルに増やし、使うことで多対多リレーションの制御をどのモデルからでも楽にできる様になります。
/**
* HasManyThrough で使っているリレーションをまとめて更新するメソッド
* @param HasManyThrough $hasManyThrough Eloquentの多対多リレーションを表現したクラス
* @param array $newThroughTgtIds 更新後のhasManyThroughで参照しているキー全て
* @param string|string[]|null $reloadRelationNames
* @throws Exception
*/
public function updateHasManyThroughRelations(HasManyThrough $hasManyThrough, array $newThroughTgtIds, $reloadRelationNames=null): void
{
// HasManyThrough クラスにはリレーションに関わるが情報が入っています。
// これを利用してある一つの Eloquent モデルの中だけで中間テーブルのリレーションを追加、削除して多対多リレーションの状態を制御できます。
/** @var \Illuminate\Database\Eloquent\Model $throughModel 中間テーブルの Eloquent モデル */
$throughModel = $hasManyThrough->getParent();
/** @var string $thisKeyInThroughModel 中間テーブルで使っている$thisのテーブルを参照しているカラムの名前 */
$thisKeyInThroughModel = $hasManyThrough->getFirstKeyName();
/** @var string $hasManyThroughTgtKeyInThroughModel 中間テーブルで使っている HasManyThrogh で参照するテーブルを参照しているカラムの名前 */
$hasManyThroughTgtKeyInThroughModel = $hasManyThrough->getSecondLocalKeyName();
/** @var Collection|int[]|string[] $alreadyExistThroughModelIds 既に$thisとリレーションを持っている中間テーブルのID */
$alreadyExistThroughModelIds = $hasManyThrough->getParent()->newQuery()
->where($thisKeyInThroughModel, $this->getKey())
->get()->pluck($hasManyThrough->getForeignKeyName());
// 更新後のhasManyThroughで参照しているキー全ての中から既に存在しているリレーションのIDを除去して、追加が必要なHasManyThrough先のレコードのIDを得ます。
collect($newThroughTgtIds)->whereNotIn(null, $alreadyExistThroughModelIds)
->each(
function ($newRelateThroughTgtId) use ($thisKeyInThroughModel, $hasManyThroughTgtKeyInThroughModel, $throughModel) {
// 追加が必要な中間テーブルレコードを追加していきます。
// ここで HasManyThrough が元の情報を使うで特定のモデルのカラムやプロパティに依存せず処理を書けます。
$newThroughModel = new $throughModel();
$newThroughModel->$thisKeyInThroughModel = $this->getKey();
$newThroughModel->$hasManyThroughTgtKeyInThroughModel = $newRelateThroughTgtId;
$newThroughModel->saveOrFail();
}
);
// 既に存在しているリレーションのIDから更新後のhasManyThroughで参照しているキー全ての中を除去して、削除が必要な中間テーブルレコードのIDを得ます。
$deleteThroughModelIds = $alreadyExistThroughModelIds->whereNotIn(null, $newThroughTgtIds);
// 削除が必要なレコードの Eloquent を まとめて削除
$throughModel->newQuery()
->where($thisKeyInThroughModel, $this->getKey())
->whereIn(
$throughModel->getTable().'.'.$hasManyThrough->getForeignKeyName(),
$deleteThroughModelIds
)->delete();
// $thisがリレーションを既にロード済みの場合、データベースを変更してもリレーションが変更されません。
// load メソッドで明示的にリレーションをロードすることで再読み込みしてリレーションをデータベースに即した形にできます。
$reloadRelationNames && $this->load($reloadRelationNames);
}