作る機能

  1. 投稿一覧&詳細ページ
  2. 管理画面へのログイン機能
  3. 投稿管理(CRUD)機能
  4. ユーザーと投稿の関連付け(多対一:HasMany)
  5. 投稿のタグ分け(多対多:ManyToMany)
  6. ユーザーロール(権限)の設定)

タグ管理機能を作る

投稿と同じようにタグの管理機能を作ります。

$ php artisan make:model Tag -a

生成してできたマイグレーションファイルを編集します。
作成するテーブルはタグのデータを入れる、tagsと、投稿とタグの中間テーブルであるpost_tagです。

database/migrations/XXXX_XX_XX_XXXXXX_create_tags_table.php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateTagsTable extends Migration
{
	/**
	 * Run the migrations.
	 *
	 * @return void
	 */
	public function up()
    {
        Schema::create('tags', function (Blueprint $table) {
            $table->id();
            $table->char('slug', 50)->unique();
            $table->char('name', 50);
            $table->timestamps();
        });

        Schema::create('post_tag', function (Blueprint $table) {
            $table->increments('id');
            $table->foreignId('post_id')->constrained();
            $table->foreignId('tag_id')->constrained();
        });
    }

	/**
	 * Reverse the migrations.
	 *
	 * @return void
	 */
	public function down()
	{
		Schema::dropIfExists('post_tag');
		Schema::dropIfExists('tags');
	}
}

タグのテーブル構造

name タグの名前
slug URLで使用する英数字

次はシーダーファイルです。
中間テーブルはモデルがないのでFakerをそのまま使用します。
(アソシエーションをランダムで書き出す方法もあるようですが)

database/seeders/TagSeeder.php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Faker\Factory as Faker;

class TagSeeder extends Seeder
{
	/**
	 * Run the database seeds.
	 *
	 * @return void
	 */
	public function run()
	{
		\DB::table('tags')->insert([
			[
				'name' => 'お知らせ',
				'slug' => 'news',
				'created_at' => now(),
				'updated_at' => now()
			],[
				'name' => 'リリース',
				'slug' => 'release',
				'created_at' => now(),
				'updated_at' => now()
			],[
				'name' => 'キャンペーン',
				'slug' => 'campaign',
				'created_at' => now(),
				'updated_at' => now()
			]
		]);

		$faker = Faker::create();
		for ($i = 1; $i <= 50; $i++) {
			\DB::table('post_tag')->insert([
				'post_id' => $i,
				'tag_id' => $faker->numberBetween(1,3)
			]);
		}
	}
}

タグの管理画面は投稿とほとんど同じなので省略します。

モデルのリレーション設定

多対多の関係はどちらのモデルもbelongsToManyで設定します。

app/Models/Post.php

class Post extends Model
{
	// ...

	/**
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
     */
    public function tags()
    {
        return $this->belongsToMany(Tag::class);
    }
}

登録・編集画面

投稿の管理画面でタグを選択して保存できるように編集します。
登録・編集画面ではタグの一覧を取得して、チェックボックで表示します。

コントローラーでタグ一覧を取得します。
ビューで展開しやすいようにKeyValueで取得するのがいいと思います。
その場合、pluckを使用します。

app/Http/Controllers/Back/PostController.php

use App\Models\Tag;

class PostController extends Controller
{
	// ...
	public function create()
	{
		$tags = Tag::pluck('name', 'id')->toArray();
		return view('back.posts.create', compact('tags'));
	}
}

editも同じように設定してください。

次は取得したタグをビューで表示します。
複数のチェックボックス選択は初見だと悩みますがこんな感じです。
(Bootstrapに完全に合わせようとすると複雑になりすぎるのである程度まで。。)

resources/views/back/posts/_form.blade.php

<div class="form-group row">
	{!! Form::label('tags', 'タグ', ['class' => 'col-sm-2 control-label']) !!}
	<div class="col-sm-10">
		<div class="{{ $errors->has('tags.*') ? 'is-invalid' : '' }}">
			@foreach ($tags as $key => $tag)
				<div class="form-check form-check-inline">
					{!! Form::checkbox( 'tags[]', $key, null, ['class' => 'form-check-input', 'id' => 'tag'.$key]) !!}
					<label class="form-check-label" for="tag{{$key}}">{{ $tag }}</label>
				</div>
			@endforeach
		</div>
		@error('tags.*')
			<span class="invalid-feedback" role="alert">
				{{ $message }}
			</span>
		@enderror
	</div>
</div>

登録・編集・削除処理

登録・編集する為の準備ができたので、登録・編集と削除処理を作っていきましょう。
登録はattach、更新はsync、削除はdetachを使用します。

app/Http/Controllers/Back/PostController.php

public function store(PostRequest $request)
{
	$post = Post::create($request->all());
	// タグを追加
	$post->tags()->attach($request->tags));

	// ...
}

public function update(PostRequest $request, Post $post)
{
	// タグを更新
	$post->tags()->sync($request->tags));

	if ($post->update($request->all())) {

	// ...
}

public function destroy(Post $post)
{
	// タグを削除
	$post->tags()->detach();

	if ($post->delete()) {

	// ...
}

バリデーション

バリデーションの設定もしておきましょう。
数値であることと入力された値がtagsテーブルに存在するかチェックします。

app/Http/Requests/PostRequest.php

public function rules()
{
	return [
		// ...

		'tags.*' => 'numeric|exists:tags,id'
	];
}

public function attributes()
{
	return [
		// ...

		'tags.*' => 'タグ'
	];
}

フロントの作成

belongsToManyの表示方法もだいたい分かったと思うので、フロントでは投稿一覧をタブで切り替えるということをやってみます。
選択したタグによって絞り込み検索する必要がありますね。

ルーターの編集

「/post?tag=news」のようなパラメータURLはいけてないので、「/posts/tag/new」のようなURLでアクセスできるようにしてみましょう。
次のように新しくルートを追加します。

routes/front.php

Route::get('posts/tag/{tagSlug}', 'PostController@index')->where('tagSlug', '[a-z]+')->name('posts.index.tag');

wheretagSlugに指定できるパラメータを限定することができます。
ひとまず小文字の英字だけ受け取れるように設定しています。

コントローラー

ルーターで設定したパラメータはコントローラーの引数で受け取ることができます。
クエリはモデルのスコープで設定しているので受け取った値をそのまま渡します。
あとはビューで表示する用にタグデータを取得しておきましょう。

app/Http/Controllers/Front/PostController.php

public function index($tagSlug = null)
{
	// 公開・新しい順に表示
	$posts = Post::publicList($tagSlug);
	$tags = Tag::all();

	return view('front.posts.index', compact('posts', 'tags'));
}

ビュー

タグのタブナビゲーションを作ります。

resources/views/front/posts/index.blade.php

<ul class="nav nav-pills mb-2">
	<li class="nav-item">
		{{ link_to_route('front.posts.index', 'すべて', null, [
			'class' => 'nav-link'.
			(request()->segment(3) === null ? ' active' : '')
		]) }}
	</li>
	@foreach($tags as $tag)
		<li class="nav-item">
			{{ link_to_route('front.posts.index.tag', $tag->name, $tag->slug, [
				'class' => 'nav-link'.
				(request()->segment(3) === $tag->slug ? ' active' : '')
			]) }}
		</li>
	@endforeach
</ul>

クエリの編集

タグを絞り検索するにはwhereHasを使用すると簡単です。

app/Models/Post.php

public function scopePublicList(Builder $query, string $tagSlug = null)
{
	if ($tagSlug) {
		$query->whereHas('tags', function($query) use ($tagSlug) {
			$query->where('slug', $tagSlug);
		});
	}
	return $query
		->with('tags')
		->public()
		->latest('published_at')
		->paginate(10);
}

ということで、多対多のリレーションでタグ機能を実装することができました。
省いたところは下記URLのGitHubを確認してください。

LaravelMiniCMS2020