LaravelでToDoアプリのAPIを実装してみよう②

# 前回の記事
https://qiita.com/kent0129/private/b991b2c0c32706fd337c
こちらの記事の続きになります。

# ToDoの詳細取得機能の実装

### コントローラーの編集 
データを1件取得するメソッドをTodoControllerに書いていきましょう。

```php:TodoController.php
    /**
     * ToDo詳細
     * 
     * @param int $id
     * @return JsonResponse
     */
    public function show($id):JsonResponse
    {
        // $idが数値以外が送られてきた時にはエラー返す
        if (!is_numeric($id) || $id <= 0) {
            return response()->json(
                [
                    'code' => Response::HTTP_BAD_REQUEST,
                    'message' => 'Invalid ID'
                ],
                Response::HTTP_BAD_REQUEST
            );
        }

        $todo = Todo::find($id);

        if (!$todo) {
            return response()->json(
                [
                    'code' => Response::HTTP_NOT_FOUND,
                    'message' => 'ToDo not found'
                ],
                Response::HTTP_NOT_FOUND
            );
        }

        return response()->json($todo);
    }
```
異常系にはエラーを返すようにしている以外は1件データを取得して返す処理になっています。
今回は実装していませんが、FomeRequestを使用したりなどバリデーションで弾くようにしても良いと思います。

### ルーティングの設定
api.phpにクリエイト処理のルーティングを設定をしてください。
```php:api.php
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\TodoController;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/

Route::prefix('todo')->group(function () {
    Route::get('/', [TodoController::class, 'index']);
    Route::post('/create', [TodoController::class, 'create']);
    Route::get('/show/{id}', [TodoController::class, 'show']);
});
```
今回はデータを取得するのでGETです。

### 動作確認

http://localhost/api/todo/show/11
上記URLにGETのHTTPリクエストを行いデータが取得できるか確認を行なってください。
![スクリーンショット 2024-01-21 3.09.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/703464/8bf8276b-5795-3bf8-e697-e3dab945eaae.png)

# ToDo編集機能の実装

CRUDのU（Update）に該当する機能です。

### フォームリクエストの作成
以下のコマンドを実行してフォームリクエストを作成をしてください。
```
./vendor/bin/sail artisan make:request TodoUpdateRequest
```

以下のようにフォームリクエストを編集してください。
```php:TodoUpdateRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use Symfony\Component\HttpFoundation\Response;

class TodoUpdateRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'id' => 'required|integer|exists:id',
            'title' => 'required|string|max:255',
            'description' => 'nullable|string',
            'finished' => 'boolean',
        ];
    }

    /**
     * バリデーションが失敗した場合の処理
     *
     * @param Validator $validator
     * @return void
     *
     * @throws HttpResponseException
     */
    protected function failedValidation(Validator $validator): void
    {
        throw new HttpResponseException(response()->json([
            'code' => Response::HTTP_BAD_REQUEST,
            'error' => $validator->errors()
        ], Response::HTTP_BAD_REQUEST));
    }
}
```
更新処理ではidのバリデーションをFormRequestで書いてみました。
今は詳細機能を修正して書き方の統一しませんが、勉強以外では統一性を持たせるように記述した方が良いと思います。

### コントローラーの編集
データを1件更新するメソッドをTodoControllerに書いていきましょう。

```php:TodoController
Classの上に追記
use App\Http\Requests\TodoUpdateRequest;

    /**
     * ToDo作成
     * 
     * @param TodoCreateRequest $request
     * @return JsonResponse
     */
    public function create(TodoCreateRequest $request): JsonResponse
    {
        try {
            $todo = $this->_updateOrCreate($request);

            return response()->json(
                [
                    'code' => Response::HTTP_OK,
                    'todo' => $todo
                ],
                Response::HTTP_OK
            );
        } catch (Throwable $e) {
            Log::error($e);

            return response()->json(
                [
                    'code' => Response::HTTP_INTERNAL_SERVER_ERROR,
                    'message' => 'Internal Server Error'
                ],
                Response::HTTP_INTERNAL_SERVER_ERROR
            );
        }
    }

    /**
     * ToDo更新
     * 
     * @param TodoUpdateRequest $request
     * @return JsonResponse
     */
    public function update(TodoUpdateRequest $request): JsonResponse
    {
        try {
            $todo = $this->_updateOrCreate($request);

            return response()->json(
                [
                    'code' => Response::HTTP_OK,
                    'todo' => $todo
                ],
                Response::HTTP_OK
            );
        } catch (Throwable $e) {
            Log::error($e);

            return response()->json(
                [
                    'code' => Response::HTTP_INTERNAL_SERVER_ERROR,
                    'message' => 'Internal Server Error'
                ],
                Response::HTTP_INTERNAL_SERVER_ERROR
            );
        }
    }

    /**
     * ToDoの登録・更新処理
     * 
     * @param Request $request
     * @return Todo
     */
    private function _updateOrCreate(Request $request): Todo
    {
        $params['title'] = $request->input('title');

        if ($request->input('description')) {
            $params['description'] = $request->input('description');
        }

        if ($request->input('finished')) {
            $params['finished'] = $request->input('finished');
        }

        return Todo::updateOrCreate(['id' => $request->input('id')], $params);
    }
```
_updateOrCreateメソッドを作成して、登録処理と更新処理をまとめてしましました。
作成処理と更新処理をまとめてしまうので、作成処理も変更が必要になります。


### ルーティングの設定
api.phpにクリエイト処理のルーティングを設定をしてください。
```php:api.php
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\TodoController;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/

Route::prefix('todo')->group(function () {
    Route::get('/', [TodoController::class, 'index']);
    Route::post('/create', [TodoController::class, 'create']);
    Route::get('/show/{id}', [TodoController::class, 'show']);
    Route::put('/update', [TodoController::class, 'update']);
});
```
今回はデータを更新するのでPUTです。

### 動作確認

http://localhost/api/todo/update
上記URLにPUTのHTTPリクエストを行いデータが更新できるか確認を行なってください。

![スクリーンショット 2024-03-13 17.33.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/703464/84674812-910e-1297-ca87-66685ecd6041.png)


# ToDo削除機能の実装
CRUDのD（Delete）に該当する機能です。

### フォームリクエストの作成
以下のコマンドを実行してフォームリクエストを作成をしてください。
```
./vendor/bin/sail artisan make:request TodoIdCheckRequest
```

以下のようにフォームリクエストを編集してください。
```php:TodoUpdateRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use Symfony\Component\HttpFoundation\Response;

class TodoIdCheckRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'id' => 'required|integer|exists:todos,id',
        ];
    }

    /**
     * バリデーションが失敗した場合の処理
     *
     * @param Validator $validator
     * @return void
     *
     * @throws HttpResponseException
     */
    protected function failedValidation(Validator $validator): void
    {
        throw new HttpResponseException(response()->json([
            'code' => Response::HTTP_BAD_REQUEST,
            'error' => $validator->errors()
        ], Response::HTTP_BAD_REQUEST));
    }
}

```

### コントローラーの編集
データを1件削除するメソッドをTodoControllerに書いていきましょう。

```php:TodoController
Classの上に追記
use App\Http\Requests\TodoIdCheckRequest;

    /**
     * ToDo詳細
     * 
     * @param TodoIdCheckRequest $request
     * @return JsonResponse
     */
    public function show(TodoIdCheckRequest $request): JsonResponse
    {
        $todo = Todo::find($request->input('id'));
        return response()->json($todo);
    }

    /**
     * ToDo削除
     * 
     * @param TodoIdCheckRequest $request
     * @return JsonResponse
     */
    public function destroy(TodoIdCheckRequest $request): JsonResponse
    {
        $id = $request->input('id');
        try {
            Todo::destroy($id);

            return response()->json(
                [
                    'code' => Response::HTTP_OK,
                    'message' => 'ToDo with ID ' . $id . ' successfully deleted'
                ],
                Response::HTTP_OK
            );
        } catch (Throwable $e) {
            Log::error($e);

            return response()->json(
                [
                    'code' => Response::HTTP_INTERNAL_SERVER_ERROR,
                    'message' => 'Internal Server Error'
                ],
                Response::HTTP_INTERNAL_SERVER_ERROR
            );
        }
    }
```
FormRequestでidをチェックするようにしたため、showメソッドを変更しました。
一件のみの削除を行うため今回はdeleteではなくdestroyを使用しています。

### ルーティングの設定
api.phpにクリエイト処理のルーティングを設定をしてください。
```php:api.php
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\TodoController;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/

Route::prefix('todo')->group(function () {
    Route::get('/', [TodoController::class, 'index']);
    Route::post('/create', [TodoController::class, 'create']);
    Route::get('/show', [TodoController::class, 'show']);
    Route::post('/update', [TodoController::class, 'update']);
    Route::delete('/destroy', [TodoController::class, 'destroy']);
});
```
今回はデータを削除するのでDELETEです。

また、showメソッドの変更を行ったためルーティングも変更しています。

### 動作確認

http://localhost/api/todo/show
上記URLにGETのHTTPリクエストを行いデータが更新できるか確認を行なってください。
![スクリーンショット 2024-01-21 3.11.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/703464/eb6b2740-2af5-9b5f-5c04-f5c001a8179f.png)

http://localhost/api/todo/destroy
上記URLにDELETEのHTTPリクエストを行いデータが更新できるか確認を行なってください。
![スクリーンショット 2024-03-13 17.37.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/703464/86c2ac69-5dc0-1b7e-bcef-b338e24d2d4f.png)


# 実装終了
以上でLaravelでToDoアプリのCRUD機能APIが実装できました。
初学者向けに記事を書きましたが、わからないところがありましたら質問をしてもらえればと思います。
リポジトリを公開していますので、最終的なコードはこちらから確認していただければと思います。
https://github.com/Kent0129/laravel-todo-api-sample

# おまけ
基本的な機能の実装は終わりましたがコードの改善等を書いておきます。

## レスポンスの共通化
response()->json()での処理の共通化ができそうなのでやってしまいましょう。
Controller.phpを以下のように編集してください。
```php:Controller.php
<?php

namespace App\Http\Controllers;

use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Routing\Controller as BaseController;

class Controller extends BaseController
{
    use AuthorizesRequests, ValidatesRequests;

    /**
     * 指定されたHTTPステータスコードとデータ配列でJSONレスポンスを生成します。
     * 
     * @param int $statusCode
     * @param array $data
     * @return JsonResponse
     */
    public function jsonResponse(int $statusCode, array $data): JsonResponse
    {
        $responseArray = ['code' => $statusCode] + $data;
        return response()->json($responseArray, $statusCode);
    }
}
```

### ToDoコントローラーの変更
response()->json()と記載している箇所を変更しました。

```php:TodoController.php
<?php

namespace App\Http\Controllers;

use App\Http\Requests\TodoCreateRequest;
use App\Http\Requests\TodoIdCheckRequest;
use App\Http\Requests\TodoUpdateRequest;
use App\Models\Todo;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
use Throwable;

class TodoController extends Controller
{
    /**
     * ToDo一覧
     * 
     * @param Request $request
     * @return JsonResponse
     */
    public function index(Request $request): JsonResponse
    {
        $query = Todo::query();

        $finished = $request->input('finished');
        if ($finished === 'true') {
            $query->where('finished', true);
        } elseif ($finished === 'false') {
            $query->where('finished', false);
        }
        $todos = $query->get();
        $array = ['todos' => $todos];

        return $this->jsonResponse(Response::HTTP_OK, $array);
    }

    /**
     * ToDo作成
     * 
     * @param TodoCreateRequest $request
     * @return JsonResponse
     */
    public function create(TodoCreateRequest $request): JsonResponse
    {
        try {
            $todo = $this->_updateOrCreate($request);
            $array = ['todo' => $todo];

            return $this->jsonResponse(Response::HTTP_OK, $array);
        } catch (Throwable $e) {
            Log::error($e);
            $array = ['message' => 'Internal Server Error'];

            return $this->jsonResponse(Response::HTTP_INTERNAL_SERVER_ERROR, $array);
        }
    }

    /**
     * ToDo詳細
     * 
     * @param TodoIdCheckRequest $request
     * @return JsonResponse
     */
    public function show(TodoIdCheckRequest $request): JsonResponse
    {
        $todo = Todo::find($request->input('id'));
        $array = ['todo' => $todo];

        return $this->jsonResponse(Response::HTTP_OK, $array);
    }

    /**
     * ToDo更新
     * 
     * @param TodoUpdateRequest $request
     * @return JsonResponse
     */
    public function update(TodoUpdateRequest $request): JsonResponse
    {
        try {
            $todo = $this->_updateOrCreate($request);
            $array = ['todo' => $todo];

            return $this->jsonResponse(Response::HTTP_OK, $array);
        } catch (Throwable $e) {
            Log::error($e);
            $array = ['message' => 'Internal Server Error'];

            return $this->jsonResponse(Response::HTTP_INTERNAL_SERVER_ERROR, $array);
        }
    }

    /**
     * ToDo削除
     * 
     * @param TodoIdCheckRequest $request
     * @return JsonResponse
     */
    public function destroy(TodoIdCheckRequest $request): JsonResponse
    {
        $id = $request->input('id');
        try {
            Todo::destroy($id);
            $array = ['message' => 'ToDo with ID ' . $id . ' successfully deleted'];

            return $this->jsonResponse(Response::HTTP_OK, $array);
        } catch (Throwable $e) {
            Log::error($e);
            $array = ['message' => 'Internal Server Error'];

            return $this->jsonResponse(Response::HTTP_INTERNAL_SERVER_ERROR, $array);
        }
    }

    /**
     * ToDoの登録・更新処理
     * 
     * @param Request $request
     * @return Todo
     */
    private function _updateOrCreate(Request $request): Todo
    {
        $params['title'] = $request->input('title');

        if ($request->input('description')) {
            $params['description'] = $request->input('description');
        }

        if ($request->input('finished')) {
            $params['finished'] = $request->input('finished');
        }

        return Todo::updateOrCreate(['id' => $request->input('id')], $params);
    }
}