Laravelのリソースコントローラを使ってCRUDを1行でルーティング【実例付】


# 緒言

　Laravelには[リソースコントローラ](https://readouble.com/laravel/8.x/ja/controllers.html#resource-controllers)という機能があります。このリソースコントローラを使用すると、CRUD一式がすんなりと作成できます。
　
　利点としては、

* ルーティングを1行で済ませることができる。
* コントローラで必要なアクションが予め列挙されているため、必要十分な記述ができる。
* アクションに対するルート名も決まっている。

などが挙げられます。

　Laravelドキュメントの[リソースコントローラにより処理されるアクション](https://readouble.com/laravel/8.x/ja/controllers.html#actions-handled-by-resource-controller)という項目では、「photo」というリソースコントローラの機能は、以下の表のようにまとめられています。

|動詞|URI|アクション|ルート名|
|:---|:---|:---|:---|
|GET|/photos|index|photos.index|
|GET|/photos/create|create|photos.create|
|POST|/photos|store|photos.store|
|GET|/photos/{photo}|show|photos.show|
|GET|/photos/{photo}/edit|edit|photos.edit|
|PUT/PATCH|/photos/{photo}|update|photos.update|
|DELETE|/photos/{photo}|destroy|photos.destroy|

　アクションを簡単に説明すると、indexは全体のリスト表示、createは新規入力、storeはcreateで入力したデータを新規作成、showは詳細表示、editは既存の項目の更新入力、updateはeditで入力したデータを更新、destroyは項目の削除、といったところでしょうか。URI欄の{photo}のところにはidが入ります。
　以下、この表から、どのように実装していくかを、実例を挙げて説明します。

　今回は、「ひとこと日記」という個人用の日記帳を作成しながら解説をします。
　
　この記事では、Laravel 8.47.0を使用しています。7以下のバージョンと記述方法に違いがあることがあるので御注意ください。[PHP](https://www.php.net/)（7.3以上）と[composer](https://getcomposer.org/)を入れておいてください。

　Windows10とUbuntu20.04.2LTSで動作確認していますが、ディレクトリ区切り文字は`/`にしてあります。Windows環境では、適宜`/`を`\`に読み替えてください。

# 第1章　リソースコントローラの作成

　Laravelをインストールしていなければインストールして、「hitokoto」というプロジェクトを作成します。

```bash
composer create-project laravel/laravel hitokoto
```

　`hitokoto`というサブディレクトリ（フォルダ）ができ、その中に各種ファイルが入っています。今後はこの`hitokoto`の中で作業を行います。VS Codeでは、このフォルダを開くと作業しやすいでしょう。
　まず、DiaryControllerというリソースコントローラを作成します。今回はモデルも一緒に指定するので、` --model=Diary`をつけます。

```bash
php artisan make:controller DiaryController --resource --model=Diary
```

モデルは以下のようなメッセージで同時に作成してくれるので、予め作っておかなくとも構いません。

```
A App\Models\Diary model does not exist. Do you want to generate it? (yes/no) [yes]:
```

　以下のようなメッセージが出てきたら完了です。

```
Model created successfully.
Controller created successfully.
```

# 第2章　データベースとその他の設定

　データベースはSQLiteを使う想定で作成します。SQLiteを使用する場合は、`.env`ファイルに`DB_CONNECTION=sqlite`と指定し、データベースに関するその他の項目はコメントアウトします。

```ini:.env
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
```

　他のRDBMSを使うのであれば、コメントアウトせずに該当する項目に適宜記述してください。

　Laravelのマイグレーション機能を使ってモデルに使うテーブルを作成します。
　まず、データベースファイルを作成します。全く空のファイルをコマンド入力で作成します。Linuxならhitokotoディレクトリから

```bash
touch database/database.sqlite
```

　Wihdowsならコマンドプロンプトでhitokotoフォルダから

```dosbatch
copy nul database\database.sqlite
```

　と入力してください。これで、空のデータベースファイルができあがりました。

　次に、データベースを作成する設計図となるマイグレーションファイルを作成します。ここで、テーブル名にはモデル名の複数形小文字にする必要があります。今回はモデルが`Diary`なのでテーブル名は`diaries`になります。
　そして、テーブルを作成するときには、create_{テーブル名}_tableと指定すると、それに対応するマイグレーションファイルを作成してくれます。これらを踏まえて、以下のように入力すると、

```bash
php artisan make:migration create_diaries_table
```

database/migrationsに`XXXX_XX_XX_XXXXXX_create_diaries_table.php`というファイルが作成されます。XXXの部分には作成年月日時刻が入ります。これから作るテーブルには、日付、題名、内容を保存したいので、このファイルを編集して以下のよう3行挿入します。

```php:XXXX_XX_XX_XXXXXX_create_diaries_table.php
<?php

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

class CreateDiariesTable extends Migration
{
    public function up()
    {
        Schema::create('diaries', function (Blueprint $table) {
            $table->id();
            $table->date('date'); //この行を挿入
            $table->string('title'); //この行を挿入
            $table->string('content'); //この行を挿入
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('diaries');
    }
}
```

　元からある項目のうち、idは一意なIDで、今後、編集などで使用します。timestampsは、updated_atとcreated_atという2つの列を作ります。この2つはLaravelでのデータベースに必須の列になります。
　日付はdate列にdate型で、題名はtitle列、内容はcontent列にstring型で入力するようになります。
　内容を確認したら、実際にデータベースを作成します。hitokotoディレクトリから以下のように入力してください。

```bash
php artisan migrate
```

　これでデータベースの作成は完了しました。

　このテーブルに対応する`Diary.php`というモデルは第1章で既に`app/Models`に作成しました。これはそのままで使えますので、作成されたことを確認しておけば良いでしょう。

　次に、地域設定を行います。resources/lang（laravel9はhitokoto直下のlang。larabel10では`php artisan lang:publish`を実行すると現れる）にjaディレクトリを作成しenの内容をコピーします。この中には各種メッセージの文章が入っています。いまは英語版をコピーしただけなので、中身も英語ですが、これを適当に日本語に翻訳して記述すれば、日本語のメッセージが出てくるようになります。とはいえ、全部のメッセージを翻訳するのは容易ではありません。今回は使用する分だけ、`validation.php`の`'required'`を`':attribute を入力してください。'`と置き換えるにとどめておきます。

　これで、一ヶ所しか変わっていないとはいえ、日本語の環境ができました。設定ファイルを修正します。

```php:config/app.php
'timezone' => 'Asia/Tokyo',
'locale' => 'ja',
```

　2ヶ所修正しました。一つはタイムゾーンです。これを設定しないと時刻がUTCになってしまいます。今回は日付を手入力しますが、自動的に日時を取り扱う場合には修正が必須の項目になります。
　もう一つが、ロケールを先ほど作成した日本語環境に変更しました。

# 第3章　ルーティング

　ルーティングを設定します。ここがリソースコントローラの一番の見どころなのですが、内容はごく簡単です。`routes/web.php`を修正します。このファイルを開くと、以下のように出てきます。

```php:web.php
<?php

use Illuminate\Support\Facades\Route;

（中略）

Route::get('/', function () {
    return view('welcome');
});
```

　これに2行加えます。`use Illuminate\Support\Facades\Route;`のあとに`use App\Http\Controllers\DiaryController;`を、`Route::get('/', function () {（中略）});`のあとに`Route::resource('diary', DiaryController::class);`を入れる、これだけです。


```php:web.php（修正後）
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\DiaryController; //この行を挿入

（中略）

Route::get('/', function () {
    return view('welcome');
});
Route::resource('diary', DiaryController::class); //この行を挿入
```

　これだけです。上の行はLaravel8のお約束みたいなものなので、実質、最後の1行だけで、上にあげた表の7つのルート名のルーティングを行うことができます。

# 第4章　index,create,store,showの実装
　ルートができたので、いままで放置していたコントローラとビューの実装に移ります。app/Http/Controllers/DiaryController.phpを開いてみてください。コメントを除くと以下のような構成になっています。

```php:DiaryController.php
<?php

namespace App\Http\Controllers;

use App\Models\Diary;
use Illuminate\Http\Request;

class DiaryController extends Controller
{
    public function index()
    {
    }

    public function create()
    {
    }

    public function store(Request $request)
    {
    }

    public function show(Diary $diary)
    {
    }

    public function edit(Diary $diary)
    {
    }

    public function update(Request $request, Diary $diary)
    {
    }

    public function destroy(Diary $diary)
    {
    }
}
```

　ここを埋めていけば、コントローラが完成するという訳です。

　まずは、全体のリストであるindexから手をつけます。

```php:DiaryController.php（一部）
    public function index()
    {
        $diary = new Diary;
        $list = $diary->paginate(5);
        $count = $diary->count();
        return view('diary.index',[
            'list' => $list,
            'count' => $count,
        ]);
    }
```

　引数はありません。まず必要なのは日記のリストなので、Diaryのインスタンスを作ります。5行ずつに改ページして`$list`に入れます。また、全件数を表示させたいので、`$count`に件数を入れて返します。上の表を参考にすると、返すビューは`diary.index`であることが分かりますので、そのように返します。

　これに対するビューは、`resources/views/diary/index.blade.php`となり、以下のとおり作成します。views直下ではなく、diaryというサブディレクトリを作ってその中に作成する点に注意してください。

```html:index.blade.php
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>ひとこと日記一覧</title>
        <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
    </head>
    <body>
        <div class="p-4">
        <h1 class="text-3xl">ひとこと日記一覧</h1>
        <p class="text-xl p-4">件数:{{$count}}</p>
        <p class="text-xl p-4"><a href="/diary/create" class="underline">あたらしい日記を書く</a></p>
        <table class="table-auto border-collapse">
            <thead>
                <tr>
                    <th class="px-4 py-2">ID</th>
                    <th class="px-4 py-2">日付</th>
                    <th class="px-4 py-2">題名</th>
                    <th class="px-4 py-2">編集</th>
                    <th class="px-4 py-2">削除</th>
                </tr>
            </thead>
            <tbody>
            @foreach($list as $diary)
                <tr>
                    <td class="border px-4 py-2">{{$diary->id}}</td>
                    <td class="border px-4 py-2">{{$diary->date}}</td>
                    <td class="border px-4 py-2"><a href="/diary/{{$diary->id}}" class="underline">{{$diary->title}}</a></td>
                    <td class="border px-4 py-2">
                        <form action="/diary/{{$diary->id}}/edit" method="GET">
                            @csrf
                            <button type="submit" class="border-2">編集</button>
                        </form>
                    </td>
                    <td class="border px-4 py-2">
                        <form action="/diary/{{$diary->id}}" method="POST">
                            @method('delete')
                            @csrf
                            <button type="submit" class="border-2" onclick="return confirm('「{{$diary->title}}」を削除してよろしいですか?')">削除</button>
                        </form>
                    </td>
                </tr>
            @endforeach
            </tbody>
        </table>
        <div>{{$list->links()}}</div>
        </div>
    </body>
</html>

```

　ペジネーションのリンクを使用するために、[Tailwind CSS](https://tailwindcss.jp/)をCDN経由で使用しています。内容は単純で、件数を表示したあと、見出しを表示し、`$list`で届いたリストをforeachで繰り返しているだけです。題名は詳細画面へのリンクとなっています。また、編集と削除用のボタンを用意しました。これらについては、後ほど説明します。新規作成へのリンクも必要です。新規作成ページのメソッドはGETでルート名はcreateなので、通常のリンクを作成しておきます。`@csrf`はPOSTメソッドでのクロスサイトリクエストフォージェリを防ぐためのトークンを埋め込んでいます（[公式解説](https://readouble.com/laravel/8.x/ja/blade.html#csrf-field)）。

　`DiaryController.php`に戻って、createの中身を記述します。新規記事の入力画面ですので、特にデータを渡す必要もありません。

```php:DiaryController.php（一部）
    public function create()
    {
        return view('diary.create');
    }
```

　createに対するビュー`resources/views/diary/create.blade.php`は、新規入力のためのフォームが必要となります。

```html:create.blade.php
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>あたらしい日記を書く</title>
        <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
    </head>
    <body>
        <div class="p-4">
        <h1 class="text-3xl">あたらしい日記を書く</h1>
        <form action="/diary" method="POST">
            @csrf
                <div class="p-4"><label>日付:　</label><input type="date" name="date" id=date class="border-2" value = "{{ old('date') }}"></div>
                <div class="p-4"><label>題名:　</label><input type="text" name="title" id="title" class="border-2" value = "{{ old('title') }}"></div>
                <div class="p-4"><label class="inline-block align-top">本文:　</label><textarea name="content" id="content" class="border-2" rows="5" cols="30">{{old('content')}}</textarea></div>
                <div class="p-4"><button type="submit" class="border-2">書き込む</button></div>
        </form>
        @error('date')
            <div class="p-2">{{$message}}</div>
        @enderror
        @error('title')
            <div class="p-2">{{$message}}</div>
        @enderror
        @error('content')
            <div class="p-2">{{$message}}</div>
        @enderror
        <hr />
        <div class="p-4"><a href="/diary">日記一覧へ</a></div>
        </div>
    </body>
</html>
```

　storeがPOSTで受けるので、フォームのメソッドもPOSTにします。また、各項目についてstoreでバリデーションを行います。エラーがある場合はこのビューに帰ってくるので、そのときのエラーを表示する欄を用意するのと、入力済みのデータを消さないよう、`<input>`と`<textarea>`にold関数を使用して入力データを保持しています。

　続いて、createを受けるstoreをコントローラに記述します。引数にリクエストが含まれています。言うまでもなく、createで投げたフォームの中身が入っています。

```php:DiaryController.php（一部）
    public function store(Request $request)
    {
        $diary = new Diary;
        $request->validate([
            'date' => 'required|date', //日付型か否か
            'title' => 'required|max:50', //最大50文字
            'content' => 'required|max:1000', //最大1,000文字
        ]);
        //各データを格納
        $diary->date = $request->date;
        $diary->title = $request->title;
        $diary->content = $request->content;

        //新規データとして保存
        $diary->save();

        //index用データ
        $list = $diary->paginate(5);
        $count = $diary->count();

        //indexページに戻る
        return view('diary.index',[
            'list' => $list,
            'count' => $count,
        ]);
    }
```

　まず、フォームから入ってきたデータをバリデーションを行います。dateは日付型、titleは50文字以内、contentは1,000文字以内で、いずれも必須項目となっています。もし、これらが不適合なら前のページに戻って入力し直しとなります。
　適合している場合は、$diaryの各項目に代入してsave()すれば新規作成は完了します。
　完了後はindexビューを表示するので、前回と同様に`$list`と`$count`の内容を返します。

　ここまでで、一覧と新規作成ができるようになりました。プロジェクトのルートから`php artisan serve`でサーバを起動し、「[http://127.0.0.1:8000/diary](http://127.0.0.1:8000/diary)」を開いてみてください。この時点ではデータは何も入っていないので、件数は0件で項目名だけが出てくるはずです。

　データの新規入力はできるようになったので、早速入力してみましょう。「あたらしい日記を書く」というリンクから新規入力画面を開けます。
　試しに、日付を入力せずに書き込んでみてください。先には進まず、「dateを入力してください。」と表示が出てきます。モデルの列名の関係で「日付」ではなく「date」という文字が出てくるのが少し残念ですが、ちゃんと日本語で警告が出てきました。
　6件以上入力すると、下図のようにリストが複数ページにわたっています。indexとstoreで`->paginate(5)`と記述しておいたからです。
![page.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269526/4bf3585e-2aa9-46da-adb1-4c9546492099.png)

　一覧は表示できましたが、この一覧では内容までは見られません。そもそも、内容は最大1,000文字まで許容しているので、一覧に表示させるのは現実的ではありません。そこで、詳細画面を作成して、1件ずつ見られるようにします。コントローラのshowの部分に記述を行います。

```php:DiaryController.php（一部）
    public function show(Diary $diary)
    {
        return view('diary.show',[
            'date' => $diary->date,
            'title' => $diary->title,
            'content' => $diary->content,
        ]);
    }
```

　引数にDiaryモデルである$diaryが用意されています。このshowへのリンクは、indexのタイトルの部分に用意しておきました。idを指定してあるので、当該idのデータを持ってきています。なので、そのままdate,title,contentをビューに渡せば完了です。

```html:show.blade.php
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>ひとこと日記詳細</title>
        <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
    </head>
    <body>
        <div class="p-4">
        <h1 class="text-xl">ひとこと日記詳細</h1>
        <div class="p-4"><span class="text-2xl">{{$title}}</span>　{{$date}}</div>
        <p class="p-2">{{$content}}</p>
        <hr />
        <div class="p-4"><a href="/diary">日記一覧へ</a></div>
        </div>
    </body>
</html>
```
　これで、一覧の題名をクリックすると、詳細画面が出てくるようになりました。
![2021-06-20 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269526/a1dee5f1-c11b-cf80-e011-dfd57019be7e.png)

# 第5章　edit,update,destroyの実装

　続いて、コントローラのeditの作成に移ります。createと同様の画面を作成しますが、こちらは既存のデータの表示をしなければなりません。そのため、引数に$diaryが入っています。言うまでもなく、URIで指定されたidのデータになっています。

```php:DiaryController.php（一部）
    public function edit(Diary $diary)
    {
        return view('diary.edit',[
            'id' => $diary->id,
            'date' => $diary->date,
            'title' => $diary->title,
            'content' => $diary->content,
        ]);
    }
```

　createと異なるのは、updateのURIに入れるために、idを返している点です。
　editに対するビューも作成します。

```php:edit.blade.php
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>ひとこと日記編集</title>
        <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
    </head>
    <body>
        <div class="p-4">
        <h1 class="text-3xl">ひとこと日記編集</h1>
        <form action="/diary/{{$id}}" method="POST">
            @method('PUT')
            @csrf
                <div class="p-4"><label>日付:　</label><input type="date" name="date" id=date class="border-2" value="{{$date}}"></div>
                <div class="p-4"><label>題名:　</label><input type="text" name="title" id="title" class="border-2" value="{{$title}}"></div>
                <div class="p-4"><label class="inline-block align-top">本文:　</label><textarea name="content" id="content" class="border-2" rows="5" cols="30">{{$content}}</textarea></div>
                <div class="p-4"><button type="submit" class="border-2">修正する</button></div>
        </form>
        @error('date')
            <div class="p-2">{{$message}}</div>
        @enderror
        @error('title')
            <div class="p-2">{{$message}}</div>
        @enderror
        @error('content')
            <div class="p-2">{{$message}}</div>
        @enderror
        <hr />
        <div class="p-4"><a href="/diary">日記一覧へ</a></div>
        </div>
    </body>
</html>
```

　こちらもほとんどcreateと同じです。違いは、フォームの初期データがold(xxx)ではなく、保存済みのデータであること、飛ばし先のメソッドをPUTと指定し直している点です。HTMLではGETとPOSTのメソッドしか投げられないので、ここでメソッドを書き換えている形になります。詳細は公式ドキュメントの[Methodフィールド](https://readouble.com/laravel/8.x/ja/blade.html#method-field)の項を参照してください。

　editを受けて上書き処理をするのがupdateアクションです。コントローラーにはほとんど同じ記述となりますが、こちらは引数に$diaryがあり、当該データが用意されている点が異なります。ですので、`$diary = new Diary`は必要ありません。

```php:DiaryController.php（一部）
    public function update(Request $request, Diary $diary)
    {
        $request->validate([
                'date' => 'required|date', //日付型か否か
                'title' => 'required|max:50', //最大50文字
                'content' => 'required|max:1000', //最大1,000文字
            ]);
            //各データを格納
            $diary->date = $request->date;
            $diary->title = $request->title;
            $diary->content = $request->content;

            //データ保存
            $diary->save();

            //index用データ
            $list = $diary->paginate(5);
            $count = $diary->count();

            //indexページに戻る
            return view('diary.index',[
            'list' => $list,
            'count' => $count,
        ]);
    }
```

　アップデート後にはindexを返すようにしてあります。

　最後にdestroyです。index.blade.phpの削除ボタンに割り付けてありますが、ボタンを押しただけで削除されてしまうのは乱暴なので、`onclick="return confirm('「{{$diary->title}}」を削除してよろしいですか?')"`とワンクッション入れておきました。また、DELETEメソッドもHTMLでは使えないので、`@method`を使っている点も確認してください。
　コントローラには、以下のように記述します。

```php:DiaryController.php（一部）
    public function destroy(Diary $diary)
    {
        $id = $diary->id;
        $diary->where('id',$id)->delete(); //指定したIDのデータをdeleteする。
        return redirect('/diary');
    }
```

　削除したらindexにリダイレクトしています。

# 結語
　これで、DiaryController.phpの記述が全部埋まりました。ビューは4つ作成されています。ルーティングは最初に書いた1行ですべてをカバーしています。モデルには手を触れてもいませんが、ちゃんと動いています。呆気ないくらい簡単に出来てしまいました。`php artisan serve`でサーバを起動し、操作してみてください。

　今回は日記の体を取りましたが、同様にして、データベースに入るものなら何でも、電話帳でも住所録でも、蔵書目録でも汎用メモでも作成することが出来ます。是非お試しください。
　なお、これはあくまで個人用と言うことで、誰でも読み書きできるようになっていますが、実際に公開サーバに置くときは必要なところに認証なりをかける必要があろうかと思われます。お気をつけください。

　この記事を書くにあたっては、以下のサイト及び書籍を参考にいたしました。ありがとうございます。

* [Laravel公式](http://laravel.jp/)及び[ドキュメント](https://readouble.com/laravel/)
* [LaravelでHTTPメソッド（CRUD）を使う](https://qiita.com/gochimonmon/items/a1f244ae2f7548d28de9)
* 徒然草　卜部兼好
* 枕草子　清少納言
* 更級日記　藤原孝標女
* 土左日記　紀貫之
* 紫式部日記　紫式部
* 断腸亭日乗　永井荷風