Laravel・PHP入門

PHPer初心者

【Laravel】多対多のリレーションまとめ

参考:
Eloquent:リレーション 5.5 Laravel


今回は、例えばブログのカテゴリーのような、お互いが複数関わる
Laravelの多対多の関係について言及します。
個人的にはかなりハマりましたが言及された記事が少なかった。

Laravelの多対多関係について

ブログのカテゴリー、学校の学生と科目など、ブログが多くのカテゴリーを持ち、
カテゴリーも多くのブログを持つ(この記事もLaravelや多対多、Eloquentなど多くのカテゴリを持ちます)
ような関係の事を示しています。
今回はあるエントリーとカテゴリーを例にして言及することとします。

公式ドキュメントにも記載がありますが、1対1や、1対多より少し複雑です。
2つをつなぐ中間テーブルが必要になるからです。(モデルは必要ありません)
早速実装してみます!

中間テーブルやModelを作ってみる

今回の場合、categories、entries、そしてcategory_entryテーブルを使用します。
命名規則により、多対多関係になるテーブルの単数系をアルファベット順に並べます。
(無視してもできるそうですが、かなり面倒らしいので避けることをお薦めします。)

DBの構成

  1. entriesテーブル
  • id
  • title
  • body
  • created_at
  • updated_at
  1. categoriesテーブル
  • id
  • name
  • created_at
  • updated_at
  1. category_entryテーブル(中間テーブル)
  • id
  • category_id
  • entry_id

中間テーブルについては、1つの関係につき1つのデータになりますので
このブログの記事で例をあげると、
f:id:fresh_engineer:20171209184159p:plain
この記事のidが9の場合、下記のようになります。(多いので4つに省略しています)
f:id:fresh_engineer:20171209185737p:plain:h100

つまり、1つの記事に対して4つのカテゴリが対応している場合、
4つのデータが必要になります。

DBにテーブルが登録できれば、次はmodelを作成していきます。

Modelの生成

中間テーブルについてはModelを必要としませんので、
Entryモデル、Categoryモデルを生成します。
まずはリレーション生成の部分のみ。

App\Models\Category.php

<?php

class Category extends Model
{
  public function entries()
  {
    return $this->belongsToMany('App\Models\Entry::class');
  }
}

App\Models\Entry.php

<?php
class Entry extends Model
{
  protected $fillable =
  [
    'title',
    'body',
  ];

  public function categories()
  {
    return $this->belongsToMany('App\Models\Category::class');
  }
}

今回は、多対多のため、belongsToManyメソッドを呼び出します。
準備はこれでOKです。

実践で、MVCモデルを利用した上で、データを入力する部分まで実装します。
カテゴリーは既にDBへ登録済みの前提で、
記事を投稿する時にカテゴリーの選択ができるような形で実装します。
(Viewはtwigを使用してますのでよしなに変更ください)

Controller/Model/View実装

View実装
<div class="container">
<form action="/entry/complete" method="post">

<div class="form-group">
    <label for="f-title">Title</label>
    <textarea name="title" class="form-control" id="f-other"></textarea>
</div>

<div class="form-group">
    <label for="f-category_ids">Category</label>
    <select name = "language_ids[]" class = "form-control" id = "f-language_ids" multiple>
    {% for id, name in all_categories_list %}
    <option value = "{{ id }}" >{{ name }}</option>
    {% endfor %}
    </select>
</div>

<div class="form-group">
    <label for="f-body">body</label>
    <textarea name="body" class="form-control" id="f-body"></textarea>
</div>

<div class="form-group">
    <input type="submit" value="send" class="btn" id="btn">
</div>

</form>
</div>
Controller実装

EntryControllerで入力フォームを表示する際、
既に登録済みのカテゴリーも表示します。
そして、Entryデータを保存する際に、
Entryデータと対応したカテゴリーのデータを保存するメソッドを呼び出します。

App/Controllers/EntryContoroller

<?php
public function input(Request $request) //入力のViewを表示(その時にカテゴリーの配列も渡す)
{
         return view('entry/input',
         [
                 'all_categories_list' => $this->getAllCategoriesList()
         ]);
         }
}

public function complete(Request $request)
{
         $inputs = $request->all(); 
         $category_ids = $inputs['category_ids'];
         unset($inputs['category_ids']); //fillableしているのでEntryテーブルに入らないカラムは除く

         $id = $this->entry_model->saveAndGetId($inputs);
         $this->entry_model->saveCategoryIdsFromEntryId($id, $language_ids);
         return view('entry/complete');
}
Model実装

App/Models/Entry.php

<?php
namespace App\Models;

use App\Models\Language;
use Illuminate\Database\Eloquent\Model;

class Entry extends Model
{
  protected function $fillable
       [
            'title',
            'body',
  ];

  public function saveAndGetId($inputs)
  {
          return $this->create($inputs)->id;
               //entryデータを生成した上で、中間テーブルに必要なIDを取り出す
  }
       public function saveLanguageIdsFromEntryId(int $id, array $language_ids){
            $entry = $this->find($id); //idからEntryを取り出して、
            return $entry->languages()->attach($language_ids);
            //ここで中間テーブルに保存!
        }


App/Models/Category.php

<?php
namespace App\Models;

use App\Models;
use Illuminate\Support\Collection;

class Category extends Model
{
    public function getAllCategoriesList()
    {
        return $this->pluck('name', 'id'); //すべてのカテゴリーを配列にして取り出す
    }

※関係の定義の部分は既出なので省略。


route/web.php

<?php
    Route::post('/entry/conplete' => 'EntryController@complete');

以上