Laravelで簡単なCMSを作成(記事へのタグ付け)

Laravelで簡単なCMSを作成してみます。

環境構築から記事の登録・更新は下記の記事を参考にどうぞ。

https://qiita.com/kiyo27/items/c09273bdae62422fc009

https://qiita.com/kiyo27/items/8ddb01bf5ef3d769cf66

https://qiita.com/kiyo27/items/e4bbaf96ed970be5c746

https://qiita.com/kiyo27/items/8a30a868c2644cb7ef13

今回は、作成した記事にタグを付けてみます。

## リソースコントローラ作成

Laravelのartisanコマンドを使ってリソースの作成、読み取り、更新、または削除を処理するコントローラを作成できます。

https://readouble.com/laravel/8.x/ja/controllers.html

``Tag``モデルをリソースとして、作成・読み取りができるように下記コマンドを実行してリソースコントローラーとモデルを作成します。

```
php artisan make:controller TagsController --resource --model=Tag
php artisan make:model Tag
```

``TagsController``を作成したら``index``・``create``・``store``メソッドを追加します。

```php:app/Http/Controllers/TagsController.php
<?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');
    }

    // その他のメソッド
}
```

リソースルートを登録します。

```php:routes/web.php
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``以外は使用しないのでルートから除外します。

```php:routes/web.php
// Route::resource('tags', TagsController::class);
Route::resource('tags', TagsController::class)->only([
    'index', 'create', 'store',
]);
```

## Viewを作成

``add.blade.php``と``index.blade.php``を作成してtagの登録から一覧で確認できるようにします。

```html:resources/views/tags/index.blade.php
<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>
```

```html:resources/views/tags/add.blade.php
<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をいくつか登録します。

![スクリーンショット 2021-05-02 230958.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/780549/a62dd216-ce49-bbbd-5105-513210effec9.png)
![スクリーンショット 2021-05-03 093226.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/780549/eb212d7f-772b-da16-70af-080fa2a3a8d1.png)

## 記事とタグを関連付ける

記事の登録と更新画面にタグを選択できるようにします。``articles/add.blade.php``と``articles/edit.blade.php``に以下のようにtag用のチェックボックスを追加します。

```html:
<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``メソッドで定義します。

https://readouble.com/laravel/8.x/ja/eloquent-relationships.html#many-to-many

``ArticleModel``のほうに``belongsToMany``メソッドでtagsテーブルと関連付けます。

```php:app/Models/ArticleModel.php
use App\Models\Tags;

public function tags()
{
	return $this->belongsToMany(Tag::class);
}
```

### 中間テーブルに登録する

多対多の関係である``articles``テーブルと``tag``sテーブルを関連付けるために中間テーブルの``article_tag``テーブルを使用します。記事の作成・更新時に中間テーブルにデータを更新していきます。``attach``メソッドを使用することでリレーションの中間テーブルにデータを挿入できます。``sync``メソッドを使用すると、引数に渡されたIDが中間テーブルに登録され、引数に渡されなかったIDは中間テーブルから物理削除されます。

リレーションの更新処理を``ArticlesController``に追加します。

```php:app/Http/Controllers/ArticlesController.php
    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:app/Models/ArticleModel.php
<?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();
    }
}
```

### 一覧ページ修正

記事一覧ページに、記事に関連付けられているタグも表示させます。

```html:resources/views/articles/index.blade.php
<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問題を回避することができます。

https://readouble.com/laravel/8.x/ja/eloquent-relationships.html#eager-loading

記事一覧取得時にEagerロードを使用する場合と使用しない場合で発行されるSQLを比較してみます。まずEagerロードを使用しない場合、

```php:app/Http/Controllers/ArticlesController.php
    public function index()
    {
        $articles = Article::all();
        return view('articles.index', ['articles' => $articles]);
    }
```

発行されるSQLは、まず

```sql
select * from "articles"
```

で記事を全件取得しその後記事ごとに、

```sql
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が発行されます。

![スクリーンショット 2021-05-03 085415.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/780549/2d732f4d-6a1a-4b3a-6f83-8919b279b1ec.png)
一方、Eagerロードを使用した場合、

```php:app/Http/Controllers/ArticlesController.php
    public function index()
    {
        // $articles = Article::all();
        $articles = Article::with('tags')->get();
        return view('articles.index', ['articles' => $articles]);
    }
```

発行されるSQLは2本となり激減します。
![スクリーンショット 2021-05-03 090043.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/780549/31801bab-8c82-68e0-d912-672957fffce6.png)