LaravelでRESTFullのAPIを作るシンプルな話

今回はLaravelでRESTFullなAPIってシンプルに作れなかったっけな?と言うことを思ったので簡単にぱぱぱーっと書いてみました。

開発環境

はじめに開発環境をメモしておきます。
Laravelの場合はバージョンによっては書き方が違うのでここは重要です

$ php -v
PHP 7.4.33 (cli) (built: Nov  6 2022 15:12:45) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
    with Xdebug v3.0.4, Copyright (c) 2002-2021, by Derick Rethans
    with Zend OPcache v7.4.33, Copyright (c), by Zend Technologies

$ php artisan -V
Laravel Framework 8.83.26

他は… 良いかな

MySQL 5.7が動いていますが全くバージョンは関係ありません

今回の開発対象

今回のRESTFullなAPIはSeriesデータベースをCRUD(参照、新規追加、更新、削除)するだけのシンプルなものです。

アクセスは…

これだけです。

はじめに答えを書く

いろいろありますが一番のシンプルなコントローラの実装をはじめに書きます。
ここから少しごてごて書こうと思いましたのではじめに実装できる環境をメモしておきます

はじめにAPIの Contorllerの作成のコマンドを書いておきます

$ php artisan make:controller Api/SeriesController --api

出来上がりControllerには必要なメソッド群が登録されますのでそこに動作を記述すればOKです。

シンプルなコードはこちら

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Series;
use Illuminate\Http\Request;

class SeriesController extends Controller
{
    public function index()
    {
        $items = Series::all();
        return response()->json($items,200);
    }

    public function store(Request $request)
    {
        $series = new Series();
        $series->title = $request->title;
        $series->save();
        return response()->json($series , 200);
    }

    public function show($id)
    {
        return response()->json(Series::find($id),200);
    }

    public function update(Request $request, $id)
    {
        $series = Series::find($id);
        $series->title = $request->title;
        $series->save();
        return response()->json($series , 200);
    }

    public function destroy($id)
    {
        $series = Series::destroy($id);
        return response()->json($series , 200);
    }
}

コントローラの解説は…

Eloquentの基本的な使い方ですね

  • all()で全件出ます
  • findでプライマリーキーで検索したデータを取得します
  • save()で保存します
  • destroyで対象キーのデータを削除します

リクエストURLとメソッドを書くと

  • series [GET] -> index()
  • series [POST] -> store()
  • series/id [GET] -> show()
  • series/id [POST] -> update()
  • series/id [DELETE] -> destory()

こんな感じのマッピングになります

実際に紐づけているrouteの設定

先のControllerだけではアクセスできないので動きません。
なのでroutesにマッピング情報を割り当てます
書き方はLaravel8での書き方になります (6 LTSではちょっと違います)

api.php

Route::apiResource('series',SeriesController::class);

はい、各メソッドをすべて割り当てる必要はありません
apiResourceにクラスを定義すればあんじょうよくメソッドに割り振ってくれます。
resourceでも良いとか書いていたりしますが…
APIなのでapiResourceでしょう!w

実際に登録するとルートに登録されます

$ php artisan route:list
+--------+-----------+---------------------+----------------+------------------------------------------------------------+------------+
| Domain | Method    | URI                 | Name           | Action                                                     | Middleware |
+--------+-----------+---------------------+----------------+------------------------------------------------------------+------------+
|        | GET|HEAD  | api/series          | series.index   | App\Http\Controllers\Api\SeriesController@index            | api        |
|        | POST      | api/series          | series.store   | App\Http\Controllers\Api\SeriesController@store            | api        |
|        | GET|HEAD  | api/series/{series} | series.show    | App\Http\Controllers\Api\SeriesController@show             | api        |
|        | PUT|PATCH | api/series/{series} | series.update  | App\Http\Controllers\Api\SeriesController@update           | api        |
|        | DELETE    | api/series/{series} | series.destroy | App\Http\Controllers\Api\SeriesController@destroy          | api        |

はい、前に書いたメソッド通りに定義されていることを確認できます

これで動かないですw

DBが定義されていません

データベースを作る!

はい、今回のSeriesのデータベースを作ります。

Seriesのデータベースの設計はシンプルにしました。
ずばり、idとtitleだけです。
created_atとupdated_atはデフォルトで付いていますので放置しています

$ php artisan make:migration create_series
Created Migration: 2022_11_11_080116_create_series

migrationでシンプルな入れ物ができましたのでそこに必要なカラムを追加します

class CreateSeries extends Migration
{
    /**
     * Run the migrations.
     */
    public function up()
    {
        Schema::create('series', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->timestamps();
        });
    }

idとtimestampsはデフォルトで付いていますのでtitleだけを追加します

はい、これでmigrateを実行すれば無事に作成できます!

root@be4613d8739a:/opt/tsundere# php artisan migrate
Migrating: 2022_11_11_080116_create_series
Migrated:  2022_11_11_080116_create_series (41.09ms)

はい、ここで注意があります。
migrateってlaravelが動作している環境でcallする必要があります。
なのでDockerで動かしているならdocker内に入ってからやりましょう。
秒悩みましたw

モデルクラスを作る

データベースが出来上がりましたのでアクセスするモデルを作成します。
作成の仕方はコマンド一発です

$ php artisan make:model Series
Model created successfully.

はい、これでapp/Models/Series.phpに出来上がります

でも、中身はいじる必要はありません。

Series -> seriesテーブルは標準の動作なのでクラスだけでOKです。
テーブル名が変更とかリレーショナルとかあるなら編集してください

ここまで来ると動作する

はい、ここまで書くとシンプルな動作をします。

動作の確認は私はPHPStormのHTTP Requestを使います

こうかけば動きますw


### 全件データを取得
GET http://localhost:8000/api/series

### データの指定
GET http://localhost:8000/api/series/1

### データの更新 (PUT)
PUT http://localhost:8000/api/series/4
Content-Type: application/json

{
    "title": "xxxxxxxxxxx"
}

メソッドをクリックすると実行できますし、標準出力とファイル化してくれます
動作確認をリスト化していますので全ての同時実行も可能なので動作検証には超便利ツールです

###はメソッドの区切り文字ですが後ろにコメントを追加すれば一覧に表示されると言うテクを最近知りましたw
api_test_awsはファイル名です… なんでawsで動かしているんだろう…

はい!これでシンプルなものが出来上がりました!

やっほぉ〜あとは実際に使って… はい、ここで問題です。
例えば値を変更しているtitleって必須ですが引数にデータを指定しない場合の動作ってどうします?
HTTP Requestにあります通り引数なしの場合です

### データの新規追加 (POST)引数なし
POST http://localhost:8000/api/series
Content-Type: application/json

この場合現在の状態でしたらSQLエラーで500番エラーついでにプリントスタックトレースですw

だったら、updateとsetのsaveする前に入力チェックをすれば…
こんなの

    public function store(StoreSeriesRequest $request)
    {
        if(!$request->title){
            return response()->json('エラーです' , 400);
        }
        $series = new Series();
        $series->title = $request->title;
        $series->save();
        return response()->json($series , 200);
    }

ダサいです! (動作検証はしていません。そもそもjsonの第一引数は配列だったのでエラーでしょうw)

せっかくシンプルなものを作ったのに入力チェックを入れるのは!

だったらどうするのか?FormRequestにてバリデートをさせましょう

Requestバリデートを追加する

はい、かっこよく入力チェックするにはどうすれば良いでしょうか?
そりゃもちろんFormRequestを使えばOKです。
フォームに入力してサブミットしたときに例外チェックしてエラーだったらフォームに返すそれが基本的な動きです。
今回はapiのコンテンツタイプ application/jsonでも使えますので使います

作り方はシンプルです

$ php artisan make:request Api/UpdateSeriesRequest

これで、app/Http/Requests/Api/UpdateSeriesRequest.phpにクラスが出来上がります

メソッドがauthorizeとrulesがあります.
authorize : 認証 (trueにします。たぶんfalseなら認証してから出ないと動きません)
rules : 実際のバリデートルールです

出来上がりソースはこんな感じです

use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;

class UpdateSeriesRequest extends FormRequest
{
    public function authorize()
    {
        return true;
    }
    public function rules()
    {
        return [
            'title' => 'required',
        ];
    }
    public function messages()
    {
        return [
            'title.required' => 'タイトルが未入力です',
        ];
    }

    protected function failedValidation(Validator $validator)
    {
        $res = response()->json([
            'errors' => $validator->errors(),
        ],
            400);
        throw new HttpResponseException($res);
    }
}

書いていることはシンプルです。
titleが必須です。
title必須なのでメッセージは「タイトルが未入力です」を指定します

エラーが発生した場合にはJSON書式のエラーを返します。← これ重要

ここで結構悩んだのはHttpResponseExceptionです。ex-httpパッケージではないのでご注意ください。

これでバリデートはOKって書いている記事ありますが…

え?動きませんよw
ちゃんと指定しないとw

指定の仕方はシンプルです。メソッドの引数の型を変更します

    public function update(UpdateSeriesRequest $request, $id)
    {
        $series = Series::find($id);
        $series->title = $request->title;
        $series->save();
        return response()->json($series , 200);
    }

はい、UpdateSeriesRequestの部分が変更前はRequestでした。
クラスキャストのタイミングでValidするんでしょうねw

動作を確認すると
先の引数なしの実行結果は

{
  "errors": {
    "title": [
      "タイトルが未入力です"
    ]
  }
}

とerrors要素にパラメタ(title)にエラーメッセージ(配列)が入ります

これでシンプルな処理に例外処理とかなしで動作することができました。

終いに

今回はRESTFull APIをLaravelにシンプルなものを作るメモを残しておきました

しかしながら… 個人的な主義として… ControllerにDBアクセスは書きたくないよ!w

ま、動作確認だけでしたので実装の形はもう少し違いますね

ちなみにまだ課題は残っています。
FormRequestのauthorizeです。
これを有効にして、エラーとして、認証していないとダメですよ!ってメッセージを返そうとすると…
HTMLページが返ります。

...
            <div class="ml-4 text-lg text-gray-500 uppercase tracking-wider">
                This action is unauthorized.
            </div>
...

これをなんとかできないかな… って

OpenAPI(Swagger)書き持ちながらやっていましたがまだまだですね。