Laravel と Vue と Docker でシンプルな SPA を作る ②【バックエンド】

# はじめに
この記事は、3本立ての2本目です。
- [Laravel と Vue と Docker でシンプルな SPA を作る ①【フロントエンド】](https://qiita.com/yutotakakura/items/88f0dfcb98beacb4725c)
- Laravel と Vue と Docker でシンプルな SPA を作る ②【バックエンド】__←今ここ__
- [Laravel と Vue と Docker でシンプルな SPA を作る ③【フロントエンドとバックエンドの通信】](https://qiita.com/yutotakakura/items/99831bc42dc7bc41f33a)

今回は、下記2つのコミットの内容です。後でrebaseしておきます。
- [バックエンドのAPIを実装](https://github.com/yutotakakura/laravel-vue-spa/commit/639786332cdd88fa480b82862faba404ead0992f)
- [Fix controller.](https://github.com/yutotakakura/laravel-vue-spa/commit/3ff3d591375c0d7d6a916777a3f0c33f8fe4e9c2)

# マイグレーションファイルの作成と適用
[ドキュメント](https://laravel.com/docs/10.x/migrations#generating-migrations)に従って、```Tasks```テーブルを作成するためのマイグレーションファイルを作成します。なお、DBの文字コードは[/src/config/database.php ](https://github.com/yutotakakura/laravel-vue-spa/blob/main/src/config/database.php#L74)での設定（この場合UTF-8）が反映されています。
```
$ docker-compose exec app php artisan make:migration create_tasks_table
```
作成されたマイグレーションファイル```src/database/migrations/2023_03_02_231934_create_tasks_table.php```に、テーブルを追加します。ちなみに、マイグレーションファイルの日付の部分は、作成した日時が入ります。テーブルで指定できるカラムのタイプは、[ここから](https://laravel.com/docs/10.x/migrations#available-column-types)選べます。
```diff_php
<?php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->bigIncrements('id');
+           $table->string('title', 100);
+            $table->string('content', 100);
+            $table->string('person_in_charge', 100);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('tasks');
    }
};
```
作成したマイグレーションを流します。
```
$ docker-compose exec app php artisan migrate
```
これで```Tasks```テーブルが作成されました。

# モデルの作成
LaravelのORMである[Eloquent](https://laravel.com/docs/10.x/eloquent)でDBとやり取りをするためのモデルを作成します。
[ドキュメント](https://laravel.com/docs/10.x/eloquent#generating-model-classes)に従って、モデルを作成します。
```
$ docker-compose exec app php artisan make:model Task
```
作成された```src/app/Models/Task.php```に、$fillableだけを定義しておきます。 理由の詳細は後述しますが、インサート処理に必要となるためです。
```diff_php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
    use HasFactory;

    protected $fillable = [
+        'title',
+        'content',
+        'person_in_charge',
    ];
}
```
## テストデータを作成する
後にDBとのデータ通信の挙動を確認するためにも、[Seed](https://laravel.com/docs/10.x/seeding)を使ってダミーのテストデータを10件作っておきます。
```
$ docker-compose exec app php artisan make:seeder TaskSeeder
```
作成された```src/database/seeders/TaskSeeder.php```の中でfor文を回してダミーデータを作ってもいいのですが、せっかくなのでEloquentの機能である、[Factory](https://laravel.com/docs/10.x/eloquent-factories)を使ってみます。
```
$ docker-compose exec app php artisan make:factory TaskFactory
```
生成された```src/database/factories/TaskFactory.php```に、Factoryが使っているライブラリである[FakerPHP](https://fakerphp.github.io/formatters/)のフォーマットに従って、ダミーデータを作成するメソッドと、出力先のカラムを指定します。 
```diff_php
<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Task>
 */
class TaskFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
+            'title' => fake()->word(),
+            'content' => fake()->sentence(),
+            'person_in_charge' => fake()->name(),
        ];
    }
}
```
このFactoryを、```src/database/seeders/TaskSeeder.php```で読み込みます。
```diff_php
<?php

namespace Database\Seeders;

use App\Models\Task;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class TaskSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
+        Task::factory()->count(10)->create();
    }
}
```
Factoryの定義を元にデータを作成します。countメソッドで指定した数のモデルを作成し、createでデータを永続化(DBに格納)させます。
シーディングの準備ができたので、流してみます。
```
$ docker-compose exec app php artisan db:seed
```
DBを見てみると、しっかり10件ダミーデータが作成されています。
![スクリーンショット 2023-03-08 23.13.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1447035/f7e6b1fb-e159-1072-d378-a485c407fe07.png)

# APIの実装
## タスク作成API
[ドキュメント](https://laravel.com/docs/10.x/controllers#basic-controllers)に従って、コントローラーを作成します。
```
$ docker-compose exec app php artisan make:controller TaskController
```
タスク作成APIはその名の通りタスクを作成するためのAPIです。HTTP POSTリクエストを受け取ると、リクエストのbody内のJSONデータをDBに格納します。タスク作成APIのURLは http://localhost:80/api/tasks です。リクエストのbody内のJSONは、下記のようなイメージです。
```json
{
    "title": "APIから登録",
    "content": "テストです。",
    "person_in_charge": "APIユーザー"
}
```
APIを作るためコントローラーを修正します。先ほどのコマンドで生成されたsrc/app/Http/Controllers/TaskController.phpに作成処理を行うstore()メソッドを定義します。本来は、トランザクションを張ったりしてエラーハンドリングをしないといけないのですが、今回は省略しています。
```diff_php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Task;
class TaskController extends Controller
{
    public function store(Request $request)
    {
        Task::create($request->all());
        return response()->json([
            "message" => "created successfully",
        ], Response::HTTP_CREATED);
    }
}
```
受け取ったPOSTリクエストのデータはIlluminate\Http\Requestクラスのインスタンスに格納されます。Requestインスタンスのallメソッドですべてのデータを取得し、これをTaskモデルのcreateメソッドを介してDBにデータを作成します。

ちなみに、このcreateメソッドを使う方法は、[ドキュメント](https://laravel.com/docs/10.x/eloquent#mass-assignment)にも記載がある通り、マスアサインメントから保護されています。fillableかguardedをモデルで定義しないとcreateメソッドが実行できないためです。

作成されたメソッドとルーティングの紐づけを、```src/routes/api.php```で行います。
```diff_php
+ use App\Http\Controllers\TaskController;
（省略）
+ Route::post('/tasks', [TaskController::class, 'store']);
```
[Postman](https://www.postman.com/)でリクエストを投げてみると、
![スクリーンショット 2023-03-14 8.59.47（2）.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1447035/f5f4c240-65f8-4355-de07-bc280fe20990.png)
想定通りのレスポンスを取得できました。
デバッグして少し中身を追ってみます。```$request->all()```の中身をddメソッドで見てみます。
```diff_php
    public function store(Request $request)
    {
+       dd($request->all());
        Task::create($request->all());
        return response()->json([
            "message" => "created successfully",
        ], Response::HTTP_CREATED);
    }
```
結果は、
```php
array:3 [ // app/Http/Controllers/TaskController.php:30
  "title" => "APIから登録"
  "content" => "テストです。"
  "person_in_charge" => "APIユーザー"
]
```
[ドキュメント](https://laravel.com/docs/10.x/requests#retrieving-all-input-data)に記載されている通り、リクエストのbodyにJSONで渡した値全てが配列で格納されていました。
次に、生成されるSQLを見てみます。下記の記事が参考になりました。
- [【Laravel】SQLの確認はenableQueryLogを使う](https://loglog.xyz/programming/enablequerylog)
- [Laravelで実行されるSQLを確認する2つの方法！](https://codelikes.com/confirm-laravel-sql/)

```Illuminate\Support\Facades\DB```クラスの```enableQueryLog()```と```getQueryLog()```を使ってデバッグします。
```diff_php
+ use Illuminate\Support\Facades\DB;
(省略)
    public function update(Request $request, $id)
    {
+       DB::enableQueryLog();
        Task::create($request->all());
+       dd(DB::getQueryLog());
        return response()->json([
            "message" => "created successfully",
        ], Response::HTTP_CREATED);
    }
```
結果は、
```php
array:1 [ // app/Http/Controllers/TaskController.php:32
  0 => array:3 [
    "query" => "insert into "tasks" ("title", "content", "person_in_charge", "updated_at", "created_at") values (?, ?, ?, ?, ?) returning "id""
    "bindings" => array:5 [
      0 => "APIから登録"
      1 => "テストです。"
      2 => "APIユーザー"
      3 => "2023-03-13 23:43:21"
      4 => "2023-03-13 23:43:21"
    ]
    "time" => 26.83
  ]
]
```
しっかり想定通りのINSERT文が生成されていました。
SQL文中の```returning "id"```ってなんだ？と思いましたが、PoestgreSQLの [RETURNING句](https://www.postgresql.org/docs/current/dml-returning.html) でした。MySQLしか使ったことがなかったので、初見でした。
INSERT、UPDATE、DELETE 文で変更された内容を返す機能です。下記の記事が、使いどころをわかりやすく解説してくれていました。

[知って得するPostgreSQLのRETURNING句](https://blog.ohgaki.net/postgresql-sql-returning)

## タスク全件取得API
タスク全件取得APIは、その名の通りタスクを全件取得するためのAPIです。HTTP GETリクエストを受け取ると、DBにあるレコード全件を取得して返します。タスク作成APIのURLは http://localhost:80/api/tasks です。
APIを作るためコントローラーを修正します。src/app/Http/Controllers/TaskController.phpに全件取得処理を行うindex()メソッドを定義します。
```diff_php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Task;
class TaskController extends Controller
{
+    public function index()
+    {
+        return Task::all();
+    }

     public function store(Request $request)
     {
         dd($request->all());
         Task::create($request->all());
         return response()->json([
             "message" => "created successfully",
         ], Response::HTTP_CREATED);
     }
}
```
```src/routes/api.php```にルーティングを記載します。
```diff_php
（省略）
+ Route::get('/tasks', [TaskController::class, 'index']);

Route::post('/tasks', [TaskController::class, 'store']);
```
リクエストを投げると、
![スクリーンショット 2023-03-14 9.00.46（2）.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1447035/3fed0c8c-8738-63c2-a06c-a045b9b0920f.png)
しっかり全権がJSONで返ってきました。
クエリを確認すると、
```diff_php
    public function index()
    {
+        DB::enableQueryLog();
-        return Task::all();
+        Task::all();
+        dd(DB::getQueryLog());
    }
```
結果は、
```php
array:1 [ // app/Http/Controllers/TaskController.php:15
  0 => array:3 [
    "query" => "select * from "tasks""
    "bindings" => []
    "time" => 26.68
  ]
]
```
想定通り```tasks```テーブルから全件取得するSQLが生成されていました。

## タスク一件取得API
タスク一件取得APIは、指定したタスクを一件取得するためのAPIです。HTTP GETリクエストを受け取ると、URL上のidと一致するPKのレコードをDBから取得して返します。タスク作成APIのURLは http://localhost:80/api/tasks/{id} です。
APIを作るためコントローラーを修正します。src/app/Http/Controllers/TaskController.phpに指定タスク一件取得処理を行うshow()メソッドを定義します。

```diff_php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Task;
class TaskController extends Controller
{
    public function index()
    {
        return Task::all();
    }

+   public function show($id)
+   {
+       $task = Task::find($id);
+       if ($task) {
+           return $task;
+       } else {
+           return response()->json([
+               "message" => "Task not found",
+           ], Response::HTTP_NOT_FOUND);
+       }
+   }

    public function store(Request $request)
    {
        dd($request->all());
        Task::create($request->all());
        return response()->json([
            "message" => "created successfully",
        ], Response::HTTP_CREATED);
    }
}
```
```src/routes/api.php```にルーティングを記載します。
```diff_php
Route::get('/tasks', [TaskController::class, 'index']);

+ Route::get('/tasks/{id}', [TaskController::class, 'show']);

Route::post('/tasks', [TaskController::class, 'store']);
```
リクエストを投げてみます。まずは、正常系です。
![スクリーンショット 2023-03-14 9.03.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1447035/2a42cb7d-c01d-6083-916e-4fec7c1cb2dc.png)
問題なしです。次に、異常系です。パラメータで指定したidのタスクが存在しないパターンです。
![スクリーンショット 2023-03-14 9.03.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1447035/0f4d7df6-845c-8e8d-3973-b28e1bb95641.png)
問題なかったです。次に、SQLの中身を確認してみます。
```diff_php
    public function show($id)
    {
+       DB::enableQueryLog();
        $task = Task::find($id);
+       dd(DB::getQueryLog());
        if ($task) {
            return $task;
        } else {
            return response()->json([
                "message" => "Task not found",
            ], Response::HTTP_NOT_FOUND);
        }
    }
```
結果は、
```php
array:1 [ // app/Http/Controllers/TaskController.php:19
  0 => array:3 [
    "query" => "select * from "tasks" where "tasks"."id" = ? limit 1"
    "bindings" => array:1 [
      0 => "6"
    ]
    "time" => 36.89
  ]
]
```
想定通りのSELECT文が生成されていました。

## タスク更新API
タスク更新APIは、指定したタスクを更新するためのAPIです。HTTP PUTリクエストを受け取ると、URL上のidと一致するPKのレコードをリクエストのbodyのJSONの内容で更新します。タスク作成APIのURLは http://localhost:80/api/tasks/{id} です。
APIを作るためコントローラーを修正します。src/app/Http/Controllers/TaskController.phpに更新処理を行うupdate()メソッドを定義します。
```diff_php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Task;
class TaskController extends Controller
{
    public function index()
    {
        return Task::all();
    }

    public function show($id)
    {
        $task = Task::find($id);
        if ($task) {
            return $task;
        } else {
            return response()->json([
                "message" => "Task not found",
            ], Response::HTTP_NOT_FOUND);
        }
    }

    public function store(Request $request)
    {
        Task::create($request->all());
        return response()->json([
            "message" => "created successfully",
        ], Response::HTTP_CREATED);
    }
    
+   public function update(Request $request, $id)
+   {
+       $task = Task::find($id);
+       if ($task) {
+           $task->title = is_null($request->title) ? $task->title : $request->title;
+           $task->content = is_null($request->content) ? $task->content : $request->content;
+           $task->person_in_charge = is_null($request->person_in_charge) ? $task->person_in_charge : $request->person_in_charge;
+           $task->save();
+          return response()->json([
+               "message" => "updated successfully",
+           ], Response::HTTP_OK);
+       } else {
+           return response()->json([
+               "message" => "Task not found",
+           ], Response::HTTP_NOT_FOUND);
+       }
+   }
}
```
```Request```と```Task```モデルを受け取って、```Request```の内容で```Task```モデルに対してUPDATEをかけています。
ルーティングを追加します。
```diff_php
Route::get('/tasks', [TaskController::class, 'index']);

Route::get('/tasks/{task}', [TaskController::class, 'show']);

Route::post('/tasks', [TaskController::class, 'store']);

+ Route::put('/tasks/{task}', [TaskController::class, 'update']);
```
正常系のリクエストを投げます。
![スクリーンショット 2023-03-14 9.03.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1447035/8e1daa6a-4230-f6e1-7ae7-193926c1c73e.png)
問題ありません。次に、異常系です。パラメータで指定したidのタスクが存在しないパターンです。
![スクリーンショット 2023-03-14 9.04.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1447035/381110f1-c6a1-4cd6-4bb0-89f38f219e23.png)
これも大丈夫そうです。次に、SQLを見てみます。
```diff_php
    public function update(Request $request, $id)
    {
        $task = Task::find($id);
        if ($task) {
+           DB::enableQueryLog();
            $task->title = is_null($request->title) ? $task->title : $request->title;
            $task->content = is_null($request->content) ? $task->content : $request->content;
            $task->person_in_charge = is_null($request->person_in_charge) ? $task->person_in_charge : $request->person_in_charge;
            $task->save();
+           dd(DB::getQueryLog());
            return response()->json([
                "message" => "updated successfully",
            ], Response::HTTP_OK);
        } else {
            return response()->json([
                "message" => "Task not found",
            ], Response::HTTP_NOT_FOUND);
        }
    }
```
結果は、
```php
array:2 [ // app/Http/Controllers/TaskController.php:38
  0 => array:3 [
    "query" => "select * from "tasks" where "tasks"."id" = ? limit 1"
    "bindings" => array:1 [
      0 => "5"
    ]
    "time" => 10.07
  ]
  1 => array:3 [
    "query" => "update "tasks" set "title" = ?, "content" = ?, "person_in_charge" = ?, "updated_at" = ? where "id" = ?"
    "bindings" => array:5 [
      0 => "更新APIで更新"
      1 => "更新しました。"
      2 => "APIテストユーザー。"
      3 => "2023-03-13 12:42:57"
      4 => 5
    ]
    "time" => 2.32
  ]
]
```
想定通りのSELECT文とUPDATE文が実行されていました。

## タスク削除API
タスク削除APIは、指定したタスクを削除するためのAPIです。HTTP DELETEリクエストを受け取ると、URL上のidと一致するPKのレコードを削除します。タスク作成APIのURLは http://localhost:80/api/tasks/{id} です。
APIを作るためコントローラーを修正します。src/app/Http/Controllers/TaskController.phpに削除処理を行うdestroy()メソッドを定義します。
```diff_php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Task;
class TaskController extends Controller
{
    public function index()
    {
        return Task::all();
    }

    public function show($id)
    {
        $task = Task::find($id);
        if ($task) {
            return $task;
        } else {
            return response()->json([
                "message" => "Task not found",
            ], Response::HTTP_NOT_FOUND);
        }
    }

    public function store(Request $request)
    {
        Task::create($request->all());
        return response()->json([
            "message" => "created successfully",
        ], Response::HTTP_CREATED);
    }
    
    public function update(Request $request, $id)
    {
        $task = Task::find($id);
        if ($task) {
            $task->title = is_null($request->title) ? $task->title : $request->title;
            $task->content = is_null($request->content) ? $task->content : $request->content;
            $task->person_in_charge = is_null($request->person_in_charge) ? $task->person_in_charge : $request->person_in_charge;
            $task->save();
            return response()->json([
                "message" => "updated successfully",
            ], Response::HTTP_OK);
        } else {
            return response()->json([
                "message" => "Task not found",
            ], Response::HTTP_NOT_FOUND);
        }
    }

+   public function destroy(Task $task)
+   {
+       $task = Task::find($id);
+       if ($task) {
+           $task->delete();
+           return response()->json([
+               "message" => "deleted successfully",
+           ], Response::HTTP_OK);
+       } else {
+           return response()->json([
+               "message" => "Task not found",
+           ], Response::HTTP_NOT_FOUND);
+   }
}
```
ルーティングを追加します。
```diff_php
Route::get('/tasks', [TaskController::class, 'index']);

Route::get('/tasks/{task}', [TaskController::class, 'show']);

Route::post('/tasks', [TaskController::class, 'store']);

Route::put('/tasks/{task}', [TaskController::class, 'update']);

+ Route::delete('/tasks/{task}', [TaskController::class, 'destroy']);
```
正常系のリクエストを投げてみると、
![スクリーンショット 2023-03-14 9.06.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1447035/52293f93-84dd-cf3e-4525-93d666f63254.png)
次に異常系ですが、
![スクリーンショット 2023-03-14 9.06.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1447035/e7572e90-1c77-caa8-896c-b5a568dd6af4.png)
問題ありません。クエリを見てみます。
```diff_php
    public function destroy($id)
    {
+       DB::enableQueryLog();
        $task = Task::find($id);
        if ($task) {
            $task->delete();
+           dd(DB::getQueryLog());
            return response()->json([
                "message" => "deleted successfully",
            ], Response::HTTP_OK);
        } else {
            return response()->json([
                "message" => "Task not found",
            ], Response::HTTP_NOT_FOUND);
        }
    }
```
結果は、
```php
array:2 [ // app/Http/Controllers/TaskController.php:46
  0 => array:3 [
    "query" => "select * from "tasks" where "tasks"."id" = ? limit 1"
    "bindings" => array:1 [
      0 => "5"
    ]
    "time" => 31.23
  ]
  1 => array:3 [
    "query" => "delete from "tasks" where "id" = ?"
    "bindings" => array:1 [
      0 => 5
    ]
    "time" => 2.37
  ]
]
```
想定通りのSELECT文とDELETE文が生成されていました。
もちろんエラーハンドリングなどやるべきことはあるのですが、最低限これでAPIの実装が完了しました。

# 終わりに
次回は、今回作ったAPIを、前回作ったVueのフロントから呼び出して画面に表示する処理を実装します。Laravelのリクエスト受信やDBとのやり取りの大きな流れはわかったので、もう少し内部的な仕組みをドキュメントやソースコードでこれから深ぼっていきたいと思います。