【 Laravel 6 】投稿へのタグ付け機能

#【 Laravel 6 】投稿へのタグ付け機能

2019年11月から、[Laravel歴数ヶ月の初心者が投稿型ナレッジベースのコミュニティサイトを作る](https://qiita.com/AkiYanagimoto/items/216ea9549e758342baab "投稿型ナレッジベースのコミュニティサイトを作る")というチャレンジ中。作りたいアプリケーション→機能を因数分解→ググる→先人の轍をたどる（写経する）→ぬかるみにはまる→エラー解消の神を探す→解決を繰り返す日々( ·ㅂ·)و 。備忘録として、Qiitaに投稿しています。

SNSっぽい機能の代名詞である投稿へのタグ付を実装していきます(o´艸`)


##テーブル設計

タグ機能に関するテーブル設計はこちらのサイトを参考にさせていただきました。ｱﾘｶﾞﾄｳｺﾞｻﾞｲﾏｽ<(_ _)>

**【MySQL】タグ付け機能の実装にオススメなテーブル設計（TOXI法）その2**
**https://senews.jp/toxi2/**

postsテーブルは投稿記事を入れておく、
tagsテーブルはタグを入れる、
そして、どの投稿(post)にどのタグ(tag)が紐づいているかを繋ぐテーブル（＝「中間テーブル（結合テーブル）」と呼ばれたりする）の３つのテーブルを使います。

|テーブル| posts | 中間テーブル（結合テーブル） | tags |
|:-:|:-:|:-:|:-:|
|役割|  投稿記事を入れておくテーブル |  どの投稿(post)にどのタグ(tag)が紐づいているかの関係性を示す情報を入れておくテーブル |  タグを入れるテーブル |

この「中間テーブル」には命名のルールがあるそうで、Laravel 6.xのドキュメントによれば、下記のようになっています。

(引用：Laravel 6.x Eloquent：リレーション)
多対多
多対多の関係はhasOneとhasManyリレーションよりも多少複雑な関係です。このような関係として、ユーザー(user)が多くの役目(roles)を持ち、役目(role)も大勢のユーザー(users)に共有されるという例が挙げられます。たとえば多くのユーザーは"管理者"の役目を持っています。**users、roles、role_userの３テーブル**がこの関係には必要です。**role_userテーブルは関係するモデル名をアルファベット順に並べた**もので、**user_idとrole_idを持つ**必要があります。

↑ということで、ドキュメントに習い以下のように、テーブル、それぞれにカラムを作成していきましょう。

| posts | post_tag | tags |
|:-:|:-:|:-:|
|  id |  id |  id |
|  title |  post_id |  name |
|  body | tag_id  |  tag_id |
|  user_id |   |   |


##マイグレーションファイルの生成

投稿記事そのものを入れるpostsテーブルはすでにできている前提（[【 Laravel 6 】投稿の表示・修正・更新・削除](https://qiita.com/AkiYanagimoto/items/abc1eca888c4c3f2eb99)）ですので、今回はtagsとpost_tagの2つを作ります。

```terminal:terminal
$ php artisan make:migration create_post_tag_table
```

tagの方にはモデルも必要なので、併せて作成します。-mを最後につけることで、データベースマイグレーションと同時に生成されます。（ちなみに、--allとすると、モデル作成時にマイグレーション、Factory、コントローラーを同時に作成することができる。）

```terminal:terminal
$ php artisan make:model Tag --m
```

生成されたのは、以下の3つのファイル。

**マイグレーション**
①ﾌﾟﾛｼﾞｪｸﾄﾃﾞｨﾚｸﾄﾘ/database/migrations/YYYY_MM_DD_XXXXXX_create_tags_table.php
②ﾌﾟﾛｼﾞｪｸﾄﾃﾞｨﾚｸﾄﾘ/database/migrations/YYYY_MM_DD_XXXXXX_create_post_tag_table.php
**モデル**
③ﾌﾟﾛｼﾞｪｸﾄﾃﾞｨﾚｸﾄﾘ/app/Tag.php

すでにある、以下の２つを加えた、５つのファイルを使用します。

**マイグレーション**
④ﾌﾟﾛｼﾞｪｸﾄﾃﾞｨﾚｸﾄﾘ/database/migrations/YYYY_MM_DD_XXXXXX_create_post_table.php
**モデル**
⑤ﾌﾟﾛｼﾞｪｸﾄﾃﾞｨﾚｸﾄﾘ/app/Post.php


##マイグレーション

①と②の２つのマイグレーションファイルを、以下のように修正。

```php:
# 2019_12_05_073929_create_tags_table.php
    public function up()
    {
        Schema::create('tags', function (Blueprint $table) {
            $table->increments('id'); // Laravel6.xでは初期設定はbigIncrements担っていますが、そんなに桁数の多いデータ量が必要だとも思えないので、incrementsに直す。
            $table->timestamps();
            $table->string('name'); // この行を追加
        });
    }

# 2019_12_05_074619_create_post_tag_table.php
public function up()
    {
        Schema::create('post_tag', function (Blueprint $table) {
            $table->increments('id');
            $table->timestamps();
            $table->unsignedInteger('tag_id'); //この行を追加
            $table->unsignedInteger('post_id'); //この行を追加
            $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade'); //この行を追加
            $table->foreign('post_id')->references('id')->on('posts')->onDelete('cascade'); //この行を追加
        });
    }

```


以下のコマンドを叩いてマイグレーションを実行してテーブルを作成します(ง ´͈౪`͈)ว

```terminal:terminal
$ php artisan migrate
```


##テーブルのリレーションを設定する

テーブルのリレーションの設定には、両端にある２つのテーブル(今回の場合ではpostsとtags)のそれぞれのModelに関係性を示すコードを追加します。これだけ！！

```php:
# Post.php
public function tags()
    {
        return $this->belongsToMany('App\Tag'); 
    }

# Tag.php
public function posts()
    {
        return $this->belongsToMany('App\Post'); 
    }
```


##データの入力画面を作っていく

表側も触っていきましょう。
↓こんな感じの入力画面を作ります。投稿時に、タイトル、タグ、そして本文を記述して送信します。
<img width="877" alt="スクリーンショット 2019-12-08 12.42.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406662/828f722e-c5c9-5d45-49d3-db08adc7933b.png">

投稿画面(create.blade.php)というviewファイルをこんな感じ↓に書きます。

```php:create.blade.php
@extends('layouts.app')

@section('content')
    <div class="container mt-4">
        <div class="border p-4">
            <h1 class="h5 mb-4">
                投稿の新規作成 
            </h1>

            <form method="POST" action="{{ route('posts.store') }}">
                @csrf

                <fieldset class="mb-4">
                    <div class="form-group">
                        <label for="title">
                            タイトル
                        </label>
                        <input
                            id="title"
                            name="title"
                            class="form-control {{ $errors->has('title') ? 'is-invalid' : '' }}"
                            value="{{ old('title') }}"
                            type="text"
                        >
                        @if ($errors->has('title'))
                            <div class="invalid-feedback">
                                {{ $errors->first('title') }}
                            </div>
                        @endif
                    </div>

                    <div class="form-group">
                        <label for="tags">
                            タグ
                        </label>
                        <input
                            id="tags"
                            name="tags"
                            class="form-control {{ $errors->has('tags') ? 'is-invalid' : '' }}"
                            value="{{ old('tags') }}"
                            type="text"
                        >
                        @if ($errors->has('tags'))
                            <div class="invalid-feedback">
                                {{ $errors->first('tags') }}
                            </div>
                        @endif
                    </div>

                    <div class="form-group">
                        <label for="body">
                            本文
                        </label>

                        <textarea
                            id="body"
                            name="body"
                            class="form-control {{ $errors->has('body') ? 'is-invalid' : '' }}"
                            rows="4"
                        >{{ old('body') }}</textarea>
                        @if ($errors->has('body'))
                            <div class="invalid-feedback">
                                {{ $errors->first('body') }}
                            </div>
                        @endif
                    </div>

                    <div class="mt-5">
                        <a class="btn btn-secondary" href="{{ route('top') }}">
                            キャンセル
                        </a>
                        <button type="submit" class="btn btn-primary" >
                            投稿する
                        </button>
                    </div>

                </fieldset>
            </form>
        </div>
    </div>
@endsection

```

##ルーティング

投稿はログイン済みのユーザーにのみ許可することとしているので、middleweare auth傘下にcreate（投稿入力画面表示）とstore(DBへのレコード処理)を配置しています。

```php:web.php
Route::group(['middleware' => ['auth']], function () {

    // 投稿の作成画面の表示、作成処理、詳細画面、更新、削除
    Route::resource('posts', 'PostsController', ['only' => ['create', 'store', 'show', 'edit', 'update', 'destroy']]);

});

```

##投稿入力画面表示とDBへのレコード処理

投稿入力画面表示とDBへのレコード処理をコントローラーに記述していきます。

```php:PostController.php
namespace App\Http\Controllers;

use Illuminate\Support\Facades\Auth;　//追加
use Illuminate\Support\Facades\DB;　//追加
use App\Tag;　//追加
use App\Post;　//追加

class PostsController extends Controller
{
    // 投稿の作成画面の表示
    public function create()
    {
        return view('posts.create');
    }

    //投稿のDBへのレコード作成
    public function store(Request $request)
    {
        $post = $request->validate([
            'title' => 'required|max:50',
            'body' => 'required|max:2000',
        ]);

        // #(ハッシュタグ)で始まる単語を取得。結果は、$matchに多次元配列で代入される。
        preg_match_all('/#([a-zA-z0-9０-９ぁ-んァ-ヶ亜-熙]+)/u', $request->tags, $match);
　　　　　
        // $match[0]に#(ハッシュタグ)あり、$match[1]に#(ハッシュタグ)なしの結果が入ってくるので、$match[1]で#(ハッシュタグ)なしの結果のみを使います。
        $tags = [];
        foreach ($match[1] as $tag) {
            $record = Tag::firstOrCreate(['name' => $tag]);　// firstOrCreateメソッドで、tags_tableのnameカラムに該当のない$tagは新規登録される。
            array_push($tags, $record);　// $recordを配列に追加します(=$tags)
        };

        // 投稿に紐付けされるタグのidを配列化
        $tags_id = [];
        foreach ($tags as $tag) {
            array_push($tags_id, $tag['id']);
        };
        $post->tags()->attach($tags_id);　// 投稿ににタグ付するために、attachメソッドをつかい、モデルを結びつけている中間テーブルにレコードを挿入します。
　　　　　
　　　　　// 投稿はposts_tableへレコードしましょう。
        $post = new Post;
        $post->title = $request->title;
        $post->body = $request->body;
        $post->user_id = Auth::user()->id;
        $post->save();

        return redirect()->route('top');
    }
}
```

##|ω·`) 最後に、詰んだポイント：中間テーブル（結合テーブル）の命名ルール
前述の中間テーブルの命名ルールに付いて知らなかったため、中間テーブルをテキトーに名付けていたら、以下のエラーが出てきて焦った。

*SQLSTATE[42S02]: Base table or view not found: 1146 Table 'your_project.post_tag' doesn't exist (SQL: insert into `post_tag` (`post_id`, `tag_id`) values (74, 20), (74, 8))*

以下のドキュメントにある通り、Modelの多対多リレーションを設定するbelongsToManyメソッドの記述に、第２引数として中間テーブル（結合テーブル）名を渡してあげる方法でも解決できたようです。やはり原典は丁寧に読むべしというのが、今回の（毎度毎度）の反省です( ˃̣̣̥ω˂̣̣̥ ) 。

(引用：Laravel 6.x Eloquent：リレーション)
前に述べたようにリレーションの結合テーブルの名前を決めるため、Eloquentは２つのモデル名をアルファベット順に結合します。しかしこの規約は**自由にオーバーライド**できます。belongsToManyメソッドの**第２引数に渡して**ください。

return $this->belongsToMany('App\Role', **'role_user'**);


以上です！