🌊

[Vue.js×Laravel] MVC+Sモデルに基づくAxiosを用いたAPI通信の流れ(簡単な実装例あり)

2024/03/08に公開

こんにちは、インターン生のkenshinです。
Vue.jsとLaravelで実装を始めて約3ヶ月が経ちました。

突然ですがContollerの肥大化を防ぐために、従来のMVCモデルに対してServiceを追加したMVC+Sで実装するという話を耳にしたことがある方もいるかと思います。

しかし「概念はわかっても具体的な実装方法がよくわからない…」または「コードを見ても流れがイマイチ掴めない…」と悩んでいる方が、実はたくさんいるのではないかと思いました。
そんな方々向けに簡単な実装例を示しながら、フロントからAPIを叩いてデータフェッチやポストする流れをまとめたいと思います!

そして個人的に最初理解しづらかった、RequestにおけるprepareForValidation()の説明もします💦

前提知識

「そもそもMVC+SやAxiosって何?」って思った方に、それぞれ簡単に説明します。

MVC+Sモデル

まずMVCモデルはModel、View、Controllerに基づくソフトウェア設計モデルです。以下はそれぞれの簡単な説明です。

MVCとは

Model
ビジネスロジックが書かれており、データベースとやり取りなどを行う。
View
データの表示や入力処理などのユーザインターフェースを担う。
Controller
ViewとModelの橋渡し。ユーザからの入力をModelへ、Modelから取得したデータをViewに渡す。

このMVCモデルにおいて、ControllerがViewとModelの制御以外にビジネスロジックを担ってしまっているケースがあります。それだとコードの保守性や可読性の低下などの問題が発生してしまいます。そこでServiceを追加したMVC+Sモデルを採用することで、Controllerにベタ書きされていたビジネスロジックの部分をServiceに移すことができます。

Axios

AxiosはAPI通信(HTTP通信)を行うことができるJavaScriptのライブラリです。これによりデータのやり取りを簡単に行えます。サーバからデータを取得するときにGETリクエスト、データを送るときにPOSTリクエストなどを行います。

以上がわかったところで本題に入っていきます。

GETリクエスト

MVC+Sモデルでデータフェッチする流れを図にしました。以降、実装例と共に流れを追っていきます。この実装例ではusersテーブルからユーザ一覧を取得する例を示します。
MVC+Sでデータ取得
MVC+Sモデルでのデータ取得の流れ

1. フロントからAPIを叩く

まず下記のコードのように、Vue.js側でAPIのエンドポイントを/user/listに設定して、GETリクエストを送ります。基本的にはmethods内に書くのが良いでしょう。このresponse.dataがどのように取得されるのかを理解することが今回のゴールです!

vue
try {
    await this.$axios.get("/user/list")
        .then(response => {
            //usersにレスポンスデータを格納
            this.users = response.data;
        });
} catch (error) {
    window.alert(error);
}

2. Routing

指定されたAPIのエンドポイントが下記のようにRoutingされているとします。UserControllerのlist()メソッドに処理を渡していることがわかると思います。ここではルートに名前をつけており、'user.list'を指定することでURLの呼び出しが可能です。

web.php
Route::get('/user/list', [UserController::class, 'list'])->name('user.list');

3. Controller

ではUserControllerのlist()メソッドはどうなっているかと言うと、JsonResponseインスタンスが返されるということだけ書かれています。
(このデータをフロント側のresponse.dataに渡しています。)
このようにビジネスロジックをServiceに移行することで、Controllerがすっきりして可読性が向上していることがわかると思います。

具体的にコードを見ると、$outputDataにはUserServiceのlist()メソッドから取得したデータを格納しています。ここでUserService側にビジネスロジックが書かれていることが理解できるはずです。

UserController.php
class UserController extends Controller
{
    /**
     * @param UserService $service
     */
    public function __construct(private readonly UserService $service)
    {
    }

    /**
     * 一覧取得する
     *
     * @return JsonResponse
     */
    public function list(): JsonResponse
    {
        $outputData = $this->service->list();
        return response()->json($outputData->value, $outputData->code);
    }
}

4. Service

実際のビジネスロジックはこのUserServiceに書かれています。User一覧を取得するために、Userモデルから全てのレコードを取得します。取得できたらステータスコードを200として、OutputDataインスタンスを返します。エラーが生じうる操作をする場合は、適切な条件分岐をしてエラーに合わせたステータスコードを設定すると良いでしょう。

今回は全てのレコードを取得する処理が1行で済んでいるためあまり効果を実感できないかもしれませんが、他のテーブルとのリレーションなど複数行に渡る処理が必要な場合やメソッドが増えてきた場合にMVC+Sモデルの効果を実感しやすいはずです。

UserService.php
class UserService
{
    /**
     * 一覧取得する
     *
     * @return OutputData
     */
    public function list(): OutputData
    {
        $users = User::all();
        return OutputData::make($users, 200);
    }
}

ちなみにOutputDataインスタンスは以下のような形式です。Collection型のデータに加え、ステータスコードを含んでいます。

OutputData.php
class OutputData
{
    /**
     * 返却値
     *
     * @var Collection
     */
    public Collection $value;

    /**
     * Httpステータスコード
     *
     * @var int
     */
    public int $code;

    /**
     * @param Collection $value
     * @param int $code
     */
    protected function __construct(Collection $value, int $code)
    {
        $this->value = $value;
        $this->code = $code;
    }

    /**
     * インスタンス作成
     *
     * @param Collection $value
     * @param int $code
     * @return static
     */
    public static function make(Collection $value, int $code): static
    {
        return new static($value, $code);
    }
}

5. Model

User一覧取得をする際にModelを介しているので、Model内のコード例を示します。'name'属性がfillableに追加されており、create()やupdate()等を行えるようになっています。ここで注意することは、DB接続するためにテーブル名'users'に対して、Modelのファイル名は単数形の'User.php'にすることです。

User.php
class User extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name',
    ];
}

6. Migration(おまけ)

そもそもtableの中身がどうなっているかというと、すごくシンプルにしたら以下のようになります。idを主キーにして、nameとタイムスタンプのカラムが存在しているという例になります。

〇〇_create_users_table.php
class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
}

GETリクエストのまとめ

フロント側でGETリクエストしてからどのようにデータフェッチしているかの流れを示しました。ControllerがViewとModelを橋渡ししており、ControllerからJson形式のデータが返されていることまで理解できたら大丈夫だと思います。
(わからなかったらもう1回最初の図を見てください🙏)

POSTリクエスト(Requestを追加しました)

続いてフロント側からデータをポストしたい時の流れを説明します。Modelやtableは先ほどと同様であり、今回はユーザ名を変更する実装例を示します。基本的にはGETリクエストと同じ流れですが、今回Requestを追加してみました。こちらではRequestに焦点を当てて説明していきます


MVC+Sモデルでのデータ送信の流れ

1. フロントからAPIを叩く

今回はユーザ名をnewNameに変えるため、POSTメソッドでデータを送信します。このnewNameはVue.jsにおいてmethodsの引数として指定している想定です。データの上書きという意味ではPUTメソッドの方が正しいかもしれませんが、今回は便宜上POSTメソッドを使用させてください。(GETとPOSTの関係の方が直感的に理解しやすいかと思いまして…)

APIのエンドポイントは/user/update-name/${this.selectedUserId}を指定しており、変数selectedUserIdに応じてエンドポイントも変更されます。これはuserIdを指定して、そのuserIdに応じたname属性のレコードを変更するためです。

vue
try {
    await this.$axios.post(`/user/update-name/${this.selectedUserId}`, {
        name: newName,
    })
} catch (error) {
    window.alert(`ユーザー名の更新に失敗しました。\nエラー: ${error.message || error}`);
}

2. Routing

Routingは以下のようになっています。{id}に応じてエンドポイントが変更されます。UserControllerのupdateName()メソッドを追えば良いことがわかると思います。

web.php
Route::post('/user/update-name/{id}', [UserController::class, 'updateName'])
    ->whereNumber('id')
    ->name('user.updateName');

3. Controller

Controllerではユーザ名を変更するメソッドとしてupdateNameを定義しており、引数にUpdateNameRequestが存在している様子がわかると思います。このRequestによって、ServiceのupdateName()メソッドの引数としてフロント側から渡されたデータを指定するときに、バリデーションを行うことでデータ形式を確認します。

UserController.php
class UserController extends Controller
{
    /**
     * @param UserService $service
     */
    public function __construct(private readonly UserService $service)
    {
    }

    /**
     * ユーザ名を更新する
     *
     * @param UpdateNameRequest $request
     * @return JsonResponse
     */
    public function updateName(UpdateNameRequest $request): JsonResponse
    {
        $outputData = $this->service->updateName($request->validated());
        return response()->json($outputData->value, $outputData->code);
    }
}

4. Request(ここが肝)

個人的には今回の記事の中でここを一番説明したいです。UpdateNameRequestではバリデーションのルール等を記載しています。データ形式としては'name'属性は必須で文字列型にするべきということと、'userId'属性も必須で整数型にするべきということを満たしている必要があります。

そして個人的に最初何のために存在しているのかわかっていなかったprepareForValidation()メソッドに注目します。まず思い出して欲しいのが、フロント側ではnewNameを'name'属性の新たな値として指定していましたが、{id}についてはURLに含めているだけの状態でした。

そのためこのままだと'userId'がrequiredであるというバリデーションに引っかかってしまいます。ここまで話したらもうわかった人もいると思いますが、prepareForValidation()でバリデーション前の準備として、'userId'にrouteで指定した{id}をマージしているのです。

「こんなの見ればわかるじゃん」と思ったかもしれませんが、最初'id'という部分が'userId'と書かれていたので、まだひよっこ🐣の私にとっては$this->merge(['userId' => $this->route('userId')]);って結局何してるんだろうと思っていたわけです。そのため今回わかりやすく{id}に変えて説明しました。

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

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, mixed>
     */
    public function rules(): array
    {
        return [
            'name' => [
                'required',
                'string',
            ],
            'userId' => [
                'required',
                'integer',
            ],
        ];
    }

    /**
     * Prepare the data for validation.
     * @return void
     */
    protected function prepareForValidation(): void
    {
        $this->merge(['userId' => $this->route('id')]);
    }
}

5. Service

そしてバリデーションされたinputDataがServiceのメソッドの引数として指定されるわけです。UpdateNameRequestで'name''userId'がデータに含まれていることがわかるので、$inputDataは$inputData['name']や$inputData['userId']のようにデータを取り出すことができます。

このコード上ではそもそもURL内で指定されたuserIdのユーザが存在しない場合のエラー処理を行っています。指定したユーザが存在していればユーザ名を変更しています。
このような処理をControllerに書くと肥大化するため、Serviceに書いているというわけです。

UserService.php
class UserService
{
/**
     * ユーザ名を更新する
     *
     * @param array $inputData
     * @return OutputData
     */
    public function updateName(array $inputData): OutputData
    {
        $user = User::find($inputData['userId']);
        if (!$user) {
            return OutputData::make(collect(['error' => 'ユーザーが存在しません']), 404);
        }
        $user->update([
            'name' => $inputData['name'],
        ]);
        return OutputData::make(collect(['message' => 'ユーザ名を更新しました']), 200);
    }

}

POSTリクエストのまとめ

ModelやtableはGETリクエストと同じであるため省略します。

以上がPOSTリクエストでRequestを追加した時の流れになります。ここでは4. Requestで何をしていたかがわかれば、GETリクエストの時とさほど変わらないのですんなり理解できたはずです。データをフロント側から送信するためには避けては通れない道だと思うので、今回の記事が為になれば幸いです。

最後に

本記事ではVue.jsでAxiosを用いてAPI通信をすることでデータを取得 / 送信する流れを、簡単な実装例とともに説明しました。MVCモデルにServiceを加えてControllerの肥大化を抑えることに関しても言及しています。個人的に最初理解しづらかったRequestのprepareForValidation()に関しても理解を深めていただけたらと思います。

この流れが理解できたら次のToDoとしては、「Controller / Requestを実装したら機能テスト / 単体テストを書く」ということだと思います。テストコードはコードの品質向上に重要な要素だと思うので、チャレンジしてみてください!(要望があればもしかしたらまとめるかもしれないです)

以上で終わります。ここまでご覧いただきありがとうございました!
お疲れ様でした🙇

ソーシャルデータバンク テックブログ

Discussion