[WIP] LaravelとVueJsをOpenAPIを介して結合する - Connect Laravel and VueJs by OpenAPI

## Environment

- WSL2
- Windows11
- Docker
- Laravel 9
- Vue3
- node v16

## WIP
この記事は書きかけです。書いている途中に筆者が力尽きたので途中ですが投稿しています。更新予定は未定です。

## Summary

LaravelにはInertiaというものがありますが、これはLaravel独自のライブラリであるので、VueJs側にLaravelのコードが入ることになります。マイクロサービス化を考慮すると、Laravel側はAPIとした方が、保守性としては高くなります。

1. Laravelのバックエンドアプリケーションを作成します。Create Laravel backend.
2. OpenAPI仕様書をLaravelから出力します。Generate OpenAPI from Laravel.
3. VueJsのフロントエンドを生成します。Create VueJs frontend.
4. OpenAPI generatorでコードを生成します。Generate client code by OpenAPI generator.
5. レポジトリ層を実装します。Add repository layer.
6. いい感じに結合します。Connect both. Needs Optimization.

## Procedure

### Create Laravel backend. バックエンド構築

1. Laravelのバックエンドアプリケーションを作成します。Create Laravel App [ref: <https://readouble.com/laravel/9.x/ja/installation.html>]

    ```shell
    mkdir laravel-vuejs-app
    cd laravel-vuejs-app
    curl -s "https://laravel.build/backend" | bash
    cd backend

    ```

2. viteのsail upしているポートフォワーディングの設定を削除します。Delete port forwarding configuration for vite.

    ```yml
    # ...
            ports:
            - '${APP_PORT:-80}:80'
            # - '${VITE_PORT:-5173}:${VITE_PORT:-5173}' <- remove this
    # ...
    ```

3. マイグレーションを実行しておきます。Run migration.

    ```shell
    vendor/bin/sail up -d
    ~/backend$ vendor/bin/sail artisan migrate

    INFO  Preparing database.

    Creating migration table ............................................................... 24ms DONE

    INFO  Running migrations.

    2014_10_12_000000_create_users_table ................................................... 34ms DONE
    2014_10_12_100000_create_password_resets_table ......................................... 38ms DONE
    2019_08_19_000000_create_failed_jobs_table ............................................. 32ms DONE
    2019_12_14_000001_create_personal_access_tokens_table .................................. 64ms DONE

    ```

3. 毎回`vendor/bin/sail`するのは面倒なので、エイリアスを設定します。 Set alias if you want.

    ```shell
    $ nano ~/.bashrc


    # Add the following
    alias sail='[ -f sail ] && sh sail || sh vendor/bin/sail'
    # Finish

    $ source ~/.bashrc

    sail --help
    Laravel Sail

    Usage:
    sail COMMAND [options] [arguments]...
    ```

4. 適当なテーブルとモデルを追加します。Add example table and model.

    ```shell
    sail artisan make:migration CreateFlights

    # Edit up() method
    # This is  identical to <https://laravel.com/docs/9.x/migrations#migration-structure>
    public function up()
    {
        Schema::create('flights', function (Blueprint $table) {
            $table->id();
            $table->string('name'); # add
            $table->string('airline'); # add
            $table->timestamps();
        });
    }

    # And run migration again.
    sail artisan migrate
    ```

5. APIの作成。Create API endpoints.
    - Controllerの作成。Generate controller.

    ```shell
    sail artisan make:controller FlightController --resource
    ```

    - `/routes/api.php`にAPIの作成。Create route to `/routes/api.php`

    ```php
    Route::prefix('v1')->group(function () {
        Route::prefix('flight')->group(function () {
            Route::get('/', [FlightController::class, 'index']);

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

            Route::post('/', [FlightController::class, 'create']);

            Route::put('/{id}', [FlightController::class, 'update']);

            Route::delete('/{id}', [FlightController::class, 'destroy']);
        });
    });
    ```

    - `FlightController::index()`にactionの記載。Write controller action to `FlightController::index()`.

    ```php
    public function index()
    {
        return Flight::query()->paginate(10);
    }
    ```

    - `GET /api/v1/flight`の確認。Confirm `GET /api/v1/flight`

    ```shell
    $ curl localhost/api/v1/flight

    {"current_page":1,"data":[],"first_page_url":"http:\/\/localhost\/api\/v1\/flight?page=1","from":null,"last_page":1,"last_page_url":"http:\/\/localhost\/api\/v1\/flight?page=1","links":[{"url":null,"label":"&laquo; Previous","active":false},{"url":"http:\/\/localhost\/api\/v1\/flight?page=1","label":"1","active":true},{"url":null,"label":"Next &raquo;","active":false}],"next_page_url":null,"path":"http:\/\/localhost\/api\/v1\/flight","per_page":10,"prev_page_url":null,"to":null,"total":0}
    ```

### Generate OpenAPI from Laravel. OpenAPI仕様書の出力

Generate OpenAPI by laravel-openapi <https://vyuldashev.github.io/laravel-openapi/>.
laravel-openapiを用いて生成します。

1. Introduce laravel-openapi. laravel-openapiの導入

    ```shell
    sail composer require vyuldashev/laravel-openapi

    # Export provider.
    sail php artisan vendor:publish --provider="Vyuldashev\LaravelOpenApi\OpenApiServiceProvider" --tag="openapi-config"
    ```

2. OpenAPIの出力に必要なAttributeを記載していきます。Add `PathItem` attribute to controller.
    - タグを`config/openapi.php`に記載します。これが後々効いて来ます。 Add tag to `config/openapi.php`.

    ```php
    [
    # ...
        'tags' => [
            [
                'name' => 'flight',
                'description' => 'Flight controller action.'
            ]
        ]
    # ...
    ]

    ```

    - FlightController.php

    ```php
    #[OpenApi\PathItem]
    class FlightController extends Controller
    {
        #[OpenApi\Operation(tags: ['flight'], method: 'GET')]
        #[OpenApi\Response(factory: ListFlightResponse::class)]
        public function index()
        {
            return Flight::query()->paginate(10);
        }
        
        # ...
    }

    ```

    - Schemaを生成し、curlで確認したレスポンスの内容を記載します。Generate and fill up schema.

    ```shell
    # Auto generate schema of model.
    sail artisan openapi:make-schema Flight -m Flight
    ```

    ```php
    use Vyuldashev\LaravelOpenApi\Contracts\Reusable; # Add this

    class FlightSchema extends SchemaFactory implements Reusable
    {
        #...
    }
    ```

    ```shell
    # Create response.
    sail artisan openapi:make-response ListFlight
    ```

    ```php
    use GoldSpecDigital\ObjectOrientedOAS\Objects\MediaType;
    use GoldSpecDigital\ObjectOrientedOAS\Objects\Response;
    use GoldSpecDigital\ObjectOrientedOAS\Objects\Schema;
    use Vyuldashev\LaravelOpenApi\Factories\ResponseFactory;

    class ListFlightResponse extends ResponseFactory
    {
        public function build(): Response
        {
            $response = Schema::object()->properties(
                Schema::integer('current_page'),
                Schema::array('data'),
                Schema::string('first_page_url'),
                Schema::integer('from')->nullable(),
                Schema::integer('to')->nullable(),
                Schema::string('last_page_url'),
                Schema::array('links')->items(
                    Schema::object()->properties(
                        Schema::string('url')->nullable(),
                        Schema::string('label'),
                        Schema::boolean('active')
                    )
                ),
                Schema::string('next_page_url')->nullable(),
                Schema::string('path'),
                Schema::integer('per_page'),
                Schema::string('prev_page_url')->nullable(),
            );
            return Response::ok()
                ->content(MediaType::json()->schema($response))
                ->description('Successful response');
        }
    }

    ```

3. OpenAPIを出力します。Export OpenAPI.

    ```shell
    sail artisan openapi:generate > api.json
    ```

    ```json
    {
        "openapi": "3.0.2",
        "info": {
            "title": "Laravel",
            "version": "1.0.0"
        },
        "servers": [
            {
                "url": "http:\/\/localhost"
            }
        ],
        "paths": {
            "\/api\/v1\/flight": {
                "get": {
                    "tags": [
                        "flight"
                    ],
                    "summary": "Display a listing of the resource.",
                    "responses": {
                        "200": {
                            "description": "Successful response",
                            "content": {
                                "application\/json": {
                                    "schema": {
                                        "type": "object",
                                        "properties": {
                                            "current_page": {
                                                "type": "integer"
                                            },
                                            "data": {
                                                "type": "array"
                                            },
                                            "first_page_url": {
                                                "type": "string"
                                            },
                                            "from": {
                                                "type": "integer",
                                                "nullable": true
                                            },
                                            "to": {
                                                "type": "integer",
                                                "nullable": true
                                            },
                                            "last_page_url": {
                                                "type": "string"
                                            },
                                            "links": {
                                                "type": "array",
                                                "items": {
                                                    "type": "object",
                                                    "properties": {
                                                        "url": {
                                                            "type": "string",
                                                            "nullable": true
                                                        },
                                                        "label": {
                                                            "type": "string"
                                                        },
                                                        "active": {
                                                            "type": "boolean"
                                                        }
                                                    }
                                                }
                                            },
                                            "next_page_url": {
                                                "type": "string",
                                                "nullable": true
                                            },
                                            "path": {
                                                "type": "string"
                                            },
                                            "per_page": {
                                                "type": "integer"
                                            },
                                            "prev_page_url": {
                                                "type": "string",
                                                "nullable": true
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "tags": [
            {
                "name": "flight",
                "description": "Flight controller action."
            }
        ]
    }
    ```

### Create VueJs frontend. VueJsフロントエンドの構築

VueJsの例 <https://ja.vitejs.dev/guide/> に沿ってひな形を生成します。Craete frontend application according to <https://ja.vitejs.dev/guide/>.

```shell
$ npm create vite@latest
Need to install the following packages:
create-vite@latest
Ok to proceed? (y)
✔ Project name: … frontend
✔ Select a framework: › Vue
✔ Select a variant: › TypeScript

Scaffolding project in /home/kuroki/laravel-vuejs-app/frontend...

Done. Now run:

cd frontend
npm install
npm run dev

npm notice
npm notice New major version of npm available! 8.1.2 -> 9.2.0
npm notice Changelog: https://github.com/npm/cli/releases/tag/v9.2.0
npm notice Run npm install -g npm@9.2.0 to update!
npm notice

$ cd frontend
$ npm install
```

### Generate client code by OpenAPI generator. OpenAPI generatorによるコードの出力

typescript-fetch <https://openapi-generator.tech/docs/generators/typescript-fetch> で生成します。Generate client code by typescript-fetch option.

```shell
mkdir src/api
cp ../backend/api.json ./api.json

docker run --rm -v "${PWD}:/local" \
    -u `id -u`:`id -g` \
    openapitools/openapi-generator-cli generate \
    -i /local/api.json \
    -g typescript-fetch \
    -o /local/src/api
    --additional-properties withInterfaces=true
    
```

### Add repository layer

respotioryの定義を作成し、グローバル変数としてマウントします。
`src/repository.ts`

```typescript
import { FlightApi } from "./api";

interface AppRepository {
    flight: FlightApi
}

const getAppRespository = () => {
    flight: new FlightApi()
}
export const repository = getAppRespository();
```

`src/typing/repository.d.ts`

```typescript
import AppRepository from './repository'

declare module '@vue/runtime-core' {
  export interface ComponentCustomProperties {
    $repository: AppRepository;
  }
}
```

`src\main.ts`

```typescript
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { repository } from './repository';

const app = createApp(App);
app.config.globalProperties.$repository = repository; // 追加
app.mount('#app')

```