Laravelで簡単なCMSを作成してみます。
環境構築から記事の登録・更新は下記の記事を参考にどうぞ。
今回は、作成した記事にタグを付けてみます。
リソースコントローラ作成
Laravelのartisanコマンドを使ってリソースの作成、読み取り、更新、または削除を処理するコントローラを作成できます。
Tag
モデルをリソースとして、作成・読み取りができるように下記コマンドを実行してリソースコントローラーとモデルを作成します。
php artisan make:controller TagsController --resource --model=Tag
php artisan make:model Tag
TagsController
を作成したらindex
・create
・store
メソッドを追加します。
<?php
namespace App\Http\Controllers;
use App\Models\Tag;
use Illuminate\Http\Request;
class TagsController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
$tags = Tag::all();
return view('tags.index', ['tags' => $tags]);
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
return view('tags.add');
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$article = new Tag;
$article->title = $request->title;
$article->save();
return redirect('/tags');
}
// その他のメソッド
}
リソースルートを登録します。
use App\Http\Controllers\TagsController;
Route::resource('tags', TagsController::class);
以下のルートが自動的に作成されます。
動詞 | URI | アクション | ルート名 |
---|---|---|---|
GET | /tags | index | tags.index |
GET | /tags/create | create | tags.create |
POST | /tags | store | tags.store |
GET | /tags/{tag} | show | tags.show |
GET | /tags/{tag}/edit | edit | tags.edit |
PUT/PATCH | /tags/{tag} | update | tags.update |
DELETE | /tags/{tag} | destroy | tags.destroy |
このうち今のところ、index
・create
・store
以外は使用しないのでルートから除外します。
// Route::resource('tags', TagsController::class);
Route::resource('tags', TagsController::class)->only([
'index', 'create', 'store',
]);
Viewを作成
add.blade.php
とindex.blade.php
を作成してtagの登録から一覧で確認できるようにします。
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Tags') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200">
<table class="table-auto">
<thead>
<tr>
<th>Title</th>
<th>Updated</th>
</tr>
</thead>
<tbody>
@foreach($tags as $tag)
<tr>
<td>{{$tag->title}}</td>
<td>{{$tag->updated_at}}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
</x-app-layout>
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Tags') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200">
<form action="/tags" method="post">
@csrf
<div class="grid grid-cols-1 gap-6">
<label class="block">
<span class="text-gray-700">Title</span>
<input type="text" name="title" class="mt-1 block w-full">
</label>
<input type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline" value="send">
</div>
</form>
</div>
</div>
</div>
</div>
</x-app-layout>
/tags/create
にアクセスしてtagをいくつか登録します。
記事とタグを関連付ける
記事の登録と更新画面にタグを選択できるようにします。articles/add.blade.php
とarticles/edit.blade.php
に以下のようにtag用のチェックボックスを追加します。
<label class="block">
<span class="text-gray-700">Tag</span>
<select class="mt-1 block w-full" rows="3" name="tags">
@foreach($tags as $tag)
<option value="{{$tag->id}}">{{$tag->title}}</option>
@endforeach
</select>
</label>
モデルを関係付ける
articlesテーブルとtagsテーブルを関連付けます。1記事に複数タグ付けすることができ、また1つのタグに対して複数の記事を関連付けることができるので、articlesテーブルとtagsテーブルは多対多の関係になります。多対多の関係はbelongsToMany
メソッドで定義します。
ArticleModel
のほうにbelongsToMany
メソッドでtagsテーブルと関連付けます。
use App\Models\Tags;
public function tags()
{
return $this->belongsToMany(Tag::class);
}
中間テーブルに登録する
多対多の関係であるarticles
テーブルとtag
sテーブルを関連付けるために中間テーブルのarticle_tag
テーブルを使用します。記事の作成・更新時に中間テーブルにデータを更新していきます。attach
メソッドを使用することでリレーションの中間テーブルにデータを挿入できます。sync
メソッドを使用すると、引数に渡されたIDが中間テーブルに登録され、引数に渡されなかったIDは中間テーブルから物理削除されます。
リレーションの更新処理をArticlesController
に追加します。
public function create(Request $request)
{
$article = Article::create([
'title' => $request->title,
'body' => $request->body,
'user_id' => Auth::id(),
'slug' => $request->title,
]);
// 中間テーブルにtagを登録
$article->tags()->attach($request->tags);
return redirect('/articles');
}
public function update(Request $request, Article $article)
{
$article->fill($request->all())->save();
$article->tags()->sync($request->tags);
return redirect('/articles');
}
中間テーブルへの登録時にtimestampを追加する場合は、withTimestamps
メソッドを使用します。
<?php
namespace App\Models;
use App\Models\Tag;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
use HasFactory;
protected $fillable = [
'title',
'body',
'slug',
'user_id',
];
protected $casts = [
'user_id' => 'integer'
];
public function tags()
{
return $this->belongsToMany(Tag::class)->withTimestamps();
}
}
一覧ページ修正
記事一覧ページに、記事に関連付けられているタグも表示させます。
<table class="table-fixed">
<thead>
<tr>
<th class="w-1/4">Title</th>
<th class="w-1/4">Tag</th>
<th class="w-1/4">Author</th>
<th class="w-1/4">Updated</th>
</tr>
</thead>
<tbody>
@foreach($articles as $article)
<tr>
<td><a href="/article/edit/{{$article->id}}">{{$article->title}}</a></td>
<td>
@foreach($article->tags as $tag)
<div>
{{$tag->title}}
</div>
@endforeach
</td>
<td>{{$article->user_id}}</td>
<td>{{$article->updated_at}}</td>
</tr>
@endforeach
</tbody>
</table>
N + 1問題
laravelの動的プロパティは「遅延読み込み」なので、N+1問題が発生します。今回のように動的プロパティにアクセスすることが分かっている場合は、Eagerロードを使用してあらかじめモデルをロードすることでN+1問題を回避することができます。
記事一覧取得時にEagerロードを使用する場合と使用しない場合で発行されるSQLを比較してみます。まずEagerロードを使用しない場合、
public function index()
{
$articles = Article::all();
return view('articles.index', ['articles' => $articles]);
}
発行されるSQLは、まず
select * from "articles"
で記事を全件取得しその後記事ごとに、
select "tags".*, "article_tag"."article_id" as "pivot_article_id", "article_tag"."tag_id" as "pivot_tag_id", "article_tag"."created_at" as "pivot_created_at", "article_tag"."updated_at" as "pivot_updated_at" from "tags" inner join "article_tag" on "tags"."id" = "article_tag"."tag_id" where "article_tag"."article_id" = article_id
が発行されます。合計で記事の件数 + 1本のSQLが発行されます。
public function index()
{
// $articles = Article::all();
$articles = Article::with('tags')->get();
return view('articles.index', ['articles' => $articles]);
}