#【 Laravel 6 】投稿へのタグ付け機能
2019年11月から、Laravel歴数ヶ月の初心者が投稿型ナレッジベースのコミュニティサイトを作るというチャレンジ中。作りたいアプリケーション→機能を因数分解→ググる→先人の轍をたどる(写経する)→ぬかるみにはまる→エラー解消の神を探す→解決を繰り返す日々( ·ㅂ·)و 。備忘録として、Qiitaに投稿しています。
SNSっぽい機能の代名詞である投稿へのタグ付を実装していきます(o´艸`)
##テーブル設計
タグ機能に関するテーブル設計はこちらのサイトを参考にさせていただきました。アリガトウゴザイマス<(_ _)>
【MySQL】タグ付け機能の実装にオススメなテーブル設計(TOXI法)その2
https://senews.jp/toxi2/
postsテーブルは投稿記事を入れておく、
tagsテーブルはタグを入れる、
そして、どの投稿(post)にどのタグ(tag)が紐づいているかを繋ぐテーブル(=「中間テーブル(結合テーブル)」と呼ばれたりする)の3つのテーブルを使います。
テーブル | posts | 中間テーブル(結合テーブル) | tags |
---|---|---|---|
役割 | 投稿記事を入れておくテーブル | どの投稿(post)にどのタグ(tag)が紐づいているかの関係性を示す情報を入れておくテーブル | タグを入れるテーブル |
この「中間テーブル」には命名のルールがあるそうで、Laravel 6.xのドキュメントによれば、下記のようになっています。
(引用:Laravel 6.x Eloquent:リレーション)
多対多
多対多の関係はhasOneとhasManyリレーションよりも多少複雑な関係です。このような関係として、ユーザー(user)が多くの役目(roles)を持ち、役目(role)も大勢のユーザー(users)に共有されるという例が挙げられます。たとえば多くのユーザーは"管理者"の役目を持っています。users、roles、role_userの3テーブルがこの関係には必要です。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 】投稿の表示・修正・更新・削除)ですので、今回はtagsとpost_tagの2つを作ります。
$ php artisan make:migration create_post_tag_table
tagの方にはモデルも必要なので、併せて作成します。-mを最後につけることで、データベースマイグレーションと同時に生成されます。(ちなみに、--allとすると、モデル作成時にマイグレーション、Factory、コントローラーを同時に作成することができる。)
$ 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
すでにある、以下の2つを加えた、5つのファイルを使用します。
マイグレーション
④プロジェクトディレクトリ/database/migrations/YYYY_MM_DD_XXXXXX_create_post_table.php
モデル
⑤プロジェクトディレクトリ/app/Post.php
##マイグレーション
①と②の2つのマイグレーションファイルを、以下のように修正。
# 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'); //この行を追加
});
}
以下のコマンドを叩いてマイグレーションを実行してテーブルを作成します(ง ´͈౪`͈)ว
$ php artisan migrate
##テーブルのリレーションを設定する
テーブルのリレーションの設定には、両端にある2つのテーブル(今回の場合ではpostsとtags)のそれぞれのModelに関係性を示すコードを追加します。これだけ!!
# Post.php
public function tags()
{
return $this->belongsToMany('App\Tag');
}
# Tag.php
public function posts()
{
return $this->belongsToMany('App\Post');
}
##データの入力画面を作っていく
表側も触っていきましょう。
↓こんな感じの入力画面を作ります。投稿時に、タイトル、タグ、そして本文を記述して送信します。
投稿画面(create.blade.php)というviewファイルをこんな感じ↓に書きます。
@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へのレコード処理)を配置しています。
Route::group(['middleware' => ['auth']], function () {
// 投稿の作成画面の表示、作成処理、詳細画面、更新、削除
Route::resource('posts', 'PostsController', ['only' => ['create', 'store', 'show', 'edit', 'update', 'destroy']]);
});
##投稿入力画面表示とDBへのレコード処理
投稿入力画面表示とDBへのレコード処理をコントローラーに記述していきます。
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-90-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メソッドの記述に、第2引数として中間テーブル(結合テーブル)名を渡してあげる方法でも解決できたようです。やはり原典は丁寧に読むべしというのが、今回の(毎度毎度)の反省です( ˃̣̣̥ω˂̣̣̥ ) 。
(引用:Laravel 6.x Eloquent:リレーション)
前に述べたようにリレーションの結合テーブルの名前を決めるため、Eloquentは2つのモデル名をアルファベット順に結合します。しかしこの規約は自由にオーバーライドできます。belongsToManyメソッドの第2引数に渡してください。
return $this->belongsToMany('App\Role', 'role_user');
以上です!