2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-03-17

はじめに

この記事は、3本立ての2本目です。

今回は、下記2つのコミットの内容です。後でrebaseしておきます。

マイグレーションファイルの作成と適用

ドキュメントに従って、Tasksテーブルを作成するためのマイグレーションファイルを作成します。なお、DBの文字コードは/src/config/database.php での設定(この場合UTF-8)が反映されています。

$ docker-compose exec app php artisan make:migration create_tasks_table

作成されたマイグレーションファイルsrc/database/migrations/2023_03_02_231934_create_tasks_table.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でDBとやり取りをするためのモデルを作成します。
ドキュメントに従って、モデルを作成します。

$ docker-compose exec app php artisan make:model Task

作成されたsrc/app/Models/Task.phpに、$fillableだけを定義しておきます。 理由の詳細は後述しますが、インサート処理に必要となるためです。

<?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を使ってダミーのテストデータを10件作っておきます。

$ docker-compose exec app php artisan make:seeder TaskSeeder

作成されたsrc/database/seeders/TaskSeeder.phpの中でfor文を回してダミーデータを作ってもいいのですが、せっかくなのでEloquentの機能である、Factoryを使ってみます。

$ docker-compose exec app php artisan make:factory TaskFactory

生成されたsrc/database/factories/TaskFactory.phpに、Factoryが使っているライブラリであるFakerPHPのフォーマットに従って、ダミーデータを作成するメソッドと、出力先のカラムを指定します。

<?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で読み込みます。

<?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

APIの実装

タスク作成API

ドキュメントに従って、コントローラーを作成します。

$ 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は、下記のようなイメージです。

{
    "title": "APIから登録",
    "content": "テストです。",
    "person_in_charge": "APIユーザー"
}

APIを作るためコントローラーを修正します。先ほどのコマンドで生成されたsrc/app/Http/Controllers/TaskController.phpに作成処理を行うstore()メソッドを定義します。本来は、トランザクションを張ったりしてエラーハンドリングをしないといけないのですが、今回は省略しています。

<?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メソッドを使う方法は、ドキュメントにも記載がある通り、マスアサインメントから保護されています。fillableかguardedをモデルで定義しないとcreateメソッドが実行できないためです。

作成されたメソッドとルーティングの紐づけを、src/routes/api.phpで行います。

+ use App\Http\Controllers\TaskController;
省略
+ Route::post('/tasks', [TaskController::class, 'store']);

Postmanでリクエストを投げてみると、
スクリーンショット 2023-03-14 8.59.47(2).png
想定通りのレスポンスを取得できました。
デバッグして少し中身を追ってみます。$request->all()の中身をddメソッドで見てみます。

    public function store(Request $request)
    {
+       dd($request->all());
        Task::create($request->all());
        return response()->json([
            "message" => "created successfully",
        ], Response::HTTP_CREATED);
    }

結果は、

array:3 [ // app/Http/Controllers/TaskController.php:30
  "title" => "APIから登録"
  "content" => "テストです。"
  "person_in_charge" => "APIユーザー"
]

ドキュメントに記載されている通り、リクエストのbodyにJSONで渡した値全てが配列で格納されていました。
次に、生成されるSQLを見てみます。下記の記事が参考になりました。

Illuminate\Support\Facades\DBクラスのenableQueryLog()getQueryLog()を使ってデバッグします。

+ 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);
    }

結果は、

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句 でした。MySQLしか使ったことがなかったので、初見でした。
INSERT、UPDATE、DELETE 文で変更された内容を返す機能です。下記の記事が、使いどころをわかりやすく解説してくれていました。

知って得するPostgreSQLのRETURNING句

タスク全件取得API

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

<?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にルーティングを記載します。

省略
+ Route::get('/tasks', [TaskController::class, 'index']);

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

リクエストを投げると、
スクリーンショット 2023-03-14 9.00.46(2).png
しっかり全権がJSONで返ってきました。
クエリを確認すると、

    public function index()
    {
+        DB::enableQueryLog();
-        return Task::all();
+        Task::all();
+        dd(DB::getQueryLog());
    }

結果は、

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()メソッドを定義します。

<?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にルーティングを記載します。

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
問題なしです。次に、異常系です。パラメータで指定したidのタスクが存在しないパターンです。
スクリーンショット 2023-03-14 9.03.26.png
問題なかったです。次に、SQLの中身を確認してみます。

    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);
        }
    }

結果は、

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()メソッドを定義します。

<?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);
+       }
+   }
}

RequestTaskモデルを受け取って、Requestの内容でTaskモデルに対してUPDATEをかけています。
ルーティングを追加します。

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
問題ありません。次に、異常系です。パラメータで指定したidのタスクが存在しないパターンです。
スクリーンショット 2023-03-14 9.04.59.png
これも大丈夫そうです。次に、SQLを見てみます。

    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);
        }
    }

結果は、

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()メソッドを定義します。

<?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);
+   }
}

ルーティングを追加します。

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
次に異常系ですが、
スクリーンショット 2023-03-14 9.06.17.png
問題ありません。クエリを見てみます。

    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);
        }
    }

結果は、

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とのやり取りの大きな流れはわかったので、もう少し内部的な仕組みをドキュメントやソースコードでこれから深ぼっていきたいと思います。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?