Web API開発をするなら、ドキュメントは自動生成にしておこう！（CakePHP編）

### この記事は何？
- [PHPerKaigi2021](https://phperkaigi.jp/2021/)で発表するスライドの補足資料
- 発表で紹介したサンプルコードと同じものを作成する手順のまとめ
    - Swaggerの導入分だけ読みたい方は[こちら](https://qiita.com/AkitoTsukahara/items/5bab895b7b9943b0654f#swagger%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B)
- この記事を作成する上で参考にさせていただいた記事。（丁寧な解説でとても分かりやすかったです。&#x1f647;）
    - [CakePHP4 で Swagger3 を使って API ドキュメントを作る](https://qiita.com/katsuhiko/items/22511d5ed5eff3e4616a)
- 作成するAPIは[CakePHPクックブックのREST API](https://book.cakephp.org/4/ja/development/rest.html)を参考
- サンプルコード（コードだけみたい方はこちら）
    - https://github.com/AkitoTsukahara/swagger-cakephp

#### PHPerKaigi2021 LT発表のスライド
https://speakerdeck.com/akitotsukahara/web-apikai-fa-wosurunara-dokiyumentohazi-dong-sheng-cheng-nisiteokou-phperkaigi2021


# まずはCakePHPでAPIを用意する
#### DockerでCakePHPの環境を構築する

まだDockerをインストールされていない方はこちらを先にお読みください。
[Docker Compose のインストール](https://docs.docker.jp/compose/install.html)

CakePHPのプロジェクト作成には私が用意したDockerテンプレートを利用させていただきます。
以下のコマンドを実行してください。

```
git clone git@github.com:AkitoTsukahara/cakephp-template.git
cd cakephp-template
make create-project

# CakePHPインストール中にPermissonに関する確認が入りますので[Y]と入力してください。
Set Folder Permissions ? (Default to Y) [Y,n]? # Yを入力
```

実行が正常に完了していれば、下記のURLからCakePHPのTOPページを確認することができます。

[http://localhost/](http://localhost/)
![スクリーンショット 2021-03-27 16.19.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94596/95c511ab-b5cb-1495-e926-0f775302d9d4.png)

#### Databaseとの接続を設定する
`app_local.php`のDatasources中のhost, username, password, databaseを以下の様に変更してください。変更する値は`docker-compose.yml`のcake_dbコンテナから持ってきてます。

```app_local.php

'Datasources' => [
    'default' => [
        'host' => 'cake_db', # ここを変更しました
        /*
         * CakePHP will use the default DB port based on the driver selected
         * MySQL on MAMP uses port 8889, MAMP users will want to uncomment
         * the following line and set the port accordingly
         */
        //'port' => 'non_standard_port_number',

        'username' => 'cake', # ここを変更しました
        'password' => 'password', # ここを変更しました

        'database' => 'cakephp', # ここを変更しました
        /*
         * If not using the default 'public' schema with the PostgreSQL driver
         * set it here.
         */
        //'schema' => 'myapp',

        /*
         * You can use a DSN string to set the entire configuration
         */
        'url' => env('DATABASE_URL', null),
    ],

    /*
     * The test connection is used during the test suite.
     */
    'test' => [
        'host' => 'localhost',
        //'port' => 'non_standard_port_number',
        'username' => 'my_app',
        'password' => 'secret',
        'database' => 'test_myapp',
        //'schema' => 'myapp',
        'url' => env('DATABASE_TEST_URL', null),
    ],
],
```

#### Recipesテーブルを作成する
マイグレーションファイルを作成します

```
docker-compose exec php bin/cake bake migration CreateRecipes
```

マイグレーションファイルを編集して、フィールドを追加します。

``` XXXXXXXXXXXXXX_CreateRecipes.php
<?php
declare(strict_types=1);

use Migrations\AbstractMigration;

class CreateRecipes extends AbstractMigration
{
    /**
     * Change Method.
     *
     * More information on this method is available here:
     * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
     * @return void
     */
    public function change()
    {
        $this->table('recipes', ['id' => false, 'primary_key' => ['id']])
            ->addColumn('id', 'uuid', ['default' => null, 'null' => false])
            ->addColumn('title', 'string', ['default' => null, 'limit' => 255, 'null' => false])
            ->addColumn('description', 'text', ['default' => null, 'limit' => null, 'null' => false])
            ->addColumn('created', 'datetime', ['default' => 'CURRENT_TIMESTAMP', 'limit' => null, 'null' => false])
            ->addColumn('modified', 'datetime', ['default' => 'CURRENT_TIMESTAMP', 'update' => 'CURRENT_TIMESTAMP', 'limit' => null, 'null' => false])
            ->create();
    }
}
```
編集したら、テーブルを生成します。

```
docker-compose exec php bin/cake migrations migrate
```

#### Recipsモデルを作成する
モデルファイルを作成します。

```
docker-compose exec php bin/cake bake model Recipes
```

#### Recipeコントローラーを作成する
コントローラーを作成します

```
docker-compose exec php bin/cake bake controller Recipe --prefix Api
```

生成されたものからほとんど手を加えていませんが、APIなので`allowMethod`など追加しています。

```RecipeController.php
<?php
declare(strict_types=1);

namespace App\Controller\Api;

use App\Controller\AppController;

/**
 * Recipe Controller
 *
 * @property \App\Model\Table\RecipesTable $Recipes
 */
class RecipeController extends AppController
{

    public function initialize(): void
    {
        parent::initialize();
        $this->loadModel('Recipes');
        $this->loadComponent('RequestHandler');
    }

    public function index(): void
    {
        $recipes = $this->Recipes->find('all');

        $this->set('recipes', $recipes);
        $this->viewBuilder()->setOption('serialize', ['recipes']);
    }

    public function view(string $id): void
    {
        $recipe = $this->Recipes->get($id);
        $this->set('recipe', $recipe);
        $this->viewBuilder()->setOption('serialize', ['recipe']);
    }

    public function add(): void
    {
        $this->request->allowMethod(['post', 'put']);
        $recipe = $this->Recipes->newEntity($this->request->getData());
        if ($this->Recipes->save($recipe)) {
            $message = 'Saved';
        } else {
            $message = 'Error';
        }
        $this->set([
            'message' => $message,
            'recipe' => $recipe,
        ]);
        $this->viewBuilder()->setOption('serialize', ['recipe', 'message']);
    }

    public function edit(string $id): void
    {
        $this->request->allowMethod(['patch', 'post', 'put']);
        $recipe = $this->Recipes->get($id);
        $recipe = $this->Recipes->patchEntity($recipe, $this->request->getData());
        if ($this->Recipes->save($recipe)) {
            $message = 'Saved';
        } else {
            $message = 'Error';
        }
        $this->set([
            'message' => $message,
            'recipe' => $recipe,
        ]);
        $this->viewBuilder()->setOption('serialize', ['recipe', 'message']);
    }

    public function delete(string $id): void
    {
        $this->request->allowMethod(['delete']);
        $recipe = $this->Recipes->get($id);
        $message = 'Deleted';
        if (!$this->Recipes->delete($recipe)) {
            $message = 'Error';
        }
        $this->set('message', $message);
        $this->viewBuilder()->setOption('serialize', ['message']);
    }

}

```

#### ルーティングの設定

```config/routes.php

use Cake\Http\Middleware\BodyParserMiddleware;
// ...
$routes->scope('/api', ['prefix' => 'Api'], function (RouteBuilder $builder) {
    $builder->registerMiddleware('bodies', new BodyParserMiddleware());
    $builder->applyMiddleware('bodies');

    $builder->setExtensions(['json']);

    // Recipe
    $builder->connect('/recipe/index', ['controller' => 'Recipe', 'action' => 'index'])->setMethods(['GET']);
    $builder->connect('/recipe/view/:id', ['controller' => 'Recipe', 'action' => 'view'])->setPass(['id'])->setMethods(['GET']);
    $builder->connect('/recipe/add', ['controller' => 'Recipe', 'action' => 'add'])->setMethods(['POST']);
    $builder->connect('/recipe/edit/:id', ['controller' => 'Recipe', 'action' => 'edit'])->setPass(['id'])->setMethods(['PUT']);
    $builder->connect('/recipe/delete/:id', ['controller' => 'Recipe', 'action' => 'delete'])->setPass(['id'])->setMethods(['DELETE']);
});
// ...
```
ここまでできたらアクセスができるか確認しておきましょう。
以下のURLを開いてページが表示されていればOKです
http://localhost/api/recipe/index.json

#### Seedingでダミーデータを入れる

```
docker-compose exec php bin/cake bake seed Recipes
```

作成されたSeedファイルにダミーデータを記述していきます。

```RecipesSeed.php
<?php
declare(strict_types=1);

use Migrations\AbstractSeed;

/**
 * Recipes seed.
 */
class RecipesSeed extends AbstractSeed
{
    /**
     * Run Method.
     *
     * Write your database seeder using this method.
     *
     * More information on writing seeds is available here:
     * https://book.cakephp.org/phinx/0/en/seeding.html
     *
     * @return void
     */
    public function run()
    {
        $datetime = date('Y-m-d H:i:s');
        $data = [
            [
                'id' => 'c366f5be-360b-45cc-8282-65c80e434f72',
                'title' => 'ハンバーグ',
                'description' => 'ジューシーでご飯がすすむ定番のハンバーグレシピです。ソースの材料もケチャップ、ウスターソース、醤油の３つだけとシンプル。',
                'created' => $datetime,
                'modified' => $datetime,
            ],
            [
                'id' => '93d5ef90-be4d-4179-9311-e39bddc26427',
                'title' => '親子丼',
                'description' => 'ふんわりとろっとろな半熟仕立ての卵とだしの相性が抜群！ご飯にからんで食欲そそる、手軽なのに食べごたえもある満足度高めな一品です。',
                'created' => $datetime,
                'modified' => $datetime,
            ]
        ];

        $table = $this->table('recipes');
        $table->insert($data)->save();
    }
}
```

ダミーデータを投入してみましょう

```
docker-compose exec php bin/cake migrations seed
```
先程のURLをもう一度開くとデータが入っていることを確認できるかと思います。

# Swaggerを設定する
ここからがSwaggerを導入していく手順になります。
#### Swaggerをインストールする

```
docker-compose exec php php /usr/bin/composer require --dev zircote/swagger-php
```

#### ドキュメントファイルを生成するコマンドを追加する
この追加は必須ではありませんが、毎度ドキュメントファイルを生成するコマンドを実行するのが面倒なので簡略化したコマンドで呼び出せる様にします。

```composer.json
//...
"scripts": {
    "openapi": "openapi --output openapi.json --format y src/Controller/Api/"
},
//...
```

## APIドキュメントの作成
ここからソースコードの中にドキュメント内容を記述していきます。

#### 共通ドキュメント
新しく以下のファイルを作成してください。
`src/Controller/Api/swagger.php`

```swagger.php
<?php
declare(strict_types=1);

/**
 * @OA\Info(
 *   title="CakePHP Swagger",
 *   description="CakePHP Swagger LT API Automatically generate documental",
 *   version="1.0.0",
 * )
 */

/**
 * @OA\Server(
 *   description="localhost",
 *   url="http://localhost"
 * )
 */

/**
 * @OA\Schema(
 *   schema="default_error_response_content",
 *   type="object",
 *   @OA\Property(
 *     property="message",
 *     type="string",
 *     description="エラーメッセージ",
 *   ),
 *   example={
 *     "message"="予期しないエラーです"
 *   },
 * )
 */
```

#### APIドキュメント
Controllerの各メソッドにドキュメントを記載していきます。
今回はしっかり書いていますが、exampleなど省力してもドキュメントを生成できるので、必要に応じて調整してください。

```RecipeController.php
<?php
declare(strict_types=1);

namespace App\Controller\Api;

use App\Controller\AppController;

/**
 * Recipe Controller
 *
 * @property \App\Model\Table\RecipesTable $Recipes
 */
class RecipeController extends AppController
{

    public function initialize(): void
    {
        parent::initialize();
        $this->loadModel('Recipes');
        $this->loadComponent('RequestHandler');
    }

    /**
     * Index method
     *
     * @OA\Get(
     *   path="/api/recipe/index.json",
     *   operationId = "RecipeIndex",
     *   tags={"Recipe"},
     *   summary="レシピをすべて取得する",
     *   @OA\Response(
     *     response=200,
     *     description="OK",
     *     @OA\JsonContent(
     *       type="object",
     *       @OA\Property(
     *         property="recipes",
     *         type="array",
     *         description="レシピ一覧",
     *         @OA\Items(
     *           @OA\Property(
     *             property="id",
     *             type="string",
     *             description="レシピID",
     *           ),
     *           @OA\Property(
     *             property="title",
     *             type="string",
     *             description="レシピ名",
     *           ),
     *           @OA\Property(
     *             property="description",
     *             type="string",
     *             description="レシピ内容",
     *           ),
     *         ),
     *         example={
     *           {
     *             "id"="c366f5be-360b-45cc-8282-65c80e434f72",
     *             "title"="ハンバーグ",
     *             "description"="ジューシーでご飯がすすむ定番のハンバーグレシピです。ソースの材料もケチャップ、ウスターソース、醤油の３つだけとシンプル。",
     *           },
     *           {
     *             "id"="93d5ef90-be4d-4179-9311-e39bddc26427",
     *             "title"="親子丼",
     *             "description"="ふんわりとろっとろな半熟仕立ての卵とだしの相性が抜群！ご飯にからんで食欲そそる、手軽なのに食べごたえもある満足度高めな一品です。",
     *           },
     *         },
     *       ),
     *     ),
     *   ),
     *   @OA\Response(
     *     response="400",
     *     description="Unexpected Error",
     *     @OA\JsonContent(ref="#/components/schemas/default_error_response_content")
     *   ),
     * )
     *
     * @return void
     */
    public function index(): void
    {
        $recipes = $this->Recipes->find('all');

        $this->set('recipes', $recipes);
        $this->viewBuilder()->setOption('serialize', ['recipes']);
    }

    /**
     * View method
     *
     * @OA\Get(
     *   path="/api/recipe/view/{id}.json",
     *   operationId = "RecipeView",
     *   tags={"Recipe"},
     *   summary="レシピを参照する",
     *   @OA\Parameter(
     *     name="id",
     *     in="path",
     *     required=true,
     *     description="レシピID",
     *     @OA\Schema(type="string"),
     *     example="c366f5be-360b-45cc-8282-65c80e434f72"
     *   ),
     *   @OA\Response(
     *     response=200,
     *     description="OK",
     *     @OA\JsonContent(
     *       type="object",
     *       @OA\Property(
     *         property="recipes",
     *         type="object",
     *         description="レシピ",
     *         @OA\Property(
     *           property="id",
     *           type="string",
     *           description="レシピID",
     *         ),
     *         @OA\Property(
     *           property="title",
     *           type="string",
     *           description="レシピ名",
     *         ),
     *         @OA\Property(
     *           property="description",
     *           type="string",
     *           description="レシピ内容",
     *         ),
     *         example={
     *           "id"="c366f5be-360b-45cc-8282-65c80e434f72",
     *           "title"="ハンバーグ",
     *           "description"="ジューシーでご飯がすすむ定番のハンバーグレシピです。ソースの材料もケチャップ、ウスターソース、醤油の３つだけとシンプル。",
     *         },
     *       ),
     *     ),
     *   ),
     *   @OA\Response(
     *     response="400",
     *     description="Unexpected Error",
     *     @OA\JsonContent(ref="#/components/schemas/default_error_response_content")
     *   ),
     * )
     *
     * @param string $id id.
     * @return void
     */
    public function view(string $id): void
    {
        $recipe = $this->Recipes->get($id);
        $this->set('recipe', $recipe);
        $this->viewBuilder()->setOption('serialize', ['recipe']);
    }

    /**
     * Add method
     *
     * @OA\Post(
     *   path="/api/recipe/add.json",
     *   operationId = "RecipeAdd",
     *   tags={"Recipe"},
     *   summary="レシピを登録する",
     *   @OA\RequestBody(
     *     required=true,
     *     @OA\JsonContent(
     *       type="object",
     *       required={"title","description"},
     *       @OA\Property(
     *         property="title",
     *         type="string",
     *         description="レシピ名",
     *       ),
     *       @OA\Property(
     *         property="description",
     *         type="string",
     *         description="レシピ内容",
     *       ),
     *       example={
     *         "title"="ハンバーグ",
     *         "description"="ジューシーでご飯がすすむ定番のハンバーグレシピです。ソースの材料もケチャップ、ウスターソース、醤油の３つだけとシンプル。",
     *       },
     *     ),
     *   ),
     *   @OA\Response(
     *     response=200,
     *     description="OK",
     *     @OA\JsonContent(
     *       type="object",
     *       @OA\Property(
     *         property="recipe",
     *         type="object",
     *         description="レシピ",
     *         @OA\Property(
     *           property="id",
     *           type="string",
     *           description="ID",
     *         ),
     *         example={
     *           "id"="c366f5be-360b-45cc-8282-65c80e434f72",
     *         },
     *       ),
     *     ),
     *   ),
     *   @OA\Response(
     *     response="400",
     *     description="Unexpected Error",
     *     @OA\JsonContent(ref="#/components/schemas/default_error_response_content")
     *     ),
     *   ),
     * )
     *
     * @return void
     */
    public function add(): void
    {
        $this->request->allowMethod(['post', 'put']);
        $recipe = $this->Recipes->newEntity($this->request->getData());
        if ($this->Recipes->save($recipe)) {
            $message = 'Saved';
        } else {
            $message = 'Error';
        }
        $this->set([
            'message' => $message,
            'recipe' => $recipe,
        ]);
        $this->viewBuilder()->setOption('serialize', ['recipe', 'message']);
    }

    /**
     * Edit method
     *
     * @OA\Put(
     *   path="/api/recipe/edit/{id}.json",
     *   operationId = "RecipeEdit",
     *   tags={"Recipe"},
     *   summary="レシピを更新する",
     *   @OA\Parameter(
     *     name="id",
     *     in="path",
     *     required=true,
     *     description="レシピID",
     *     @OA\Schema(type="string"),
     *     example="c366f5be-360b-45cc-8282-65c80e434f72"
     *   ),
     *   @OA\RequestBody(
     *     required=true,
     *     @OA\JsonContent(
     *       type="object",
     *       required={"title", "description"},
     *       @OA\Property(
     *         property="title",
     *         type="string",
     *         description="レシピ名",
     *       ),
     *       @OA\Property(
     *         property="description",
     *         type="string",
     *         description="レシピ内容",
     *       ),
     *       example={
     *         "title"="ハンバーグ",
     *         "description"="ジューシーでご飯がすすむ定番のハンバーグレシピです。ソースの材料もケチャップ、ウスターソース、醤油の３つだけとシンプル。",
     *       },
     *     ),
     *   ),
     *   @OA\Response(
     *     response=200,
     *     description="OK",
     *     @OA\JsonContent(
     *       type="object",
     *       @OA\Property(
     *         property="recipe",
     *         type="object",
     *         description="レシピ",
     *         @OA\Property(
     *           property="id",
     *           type="string",
     *           description="ID",
     *         ),
     *         example={
     *           "id"="c366f5be-360b-45cc-8282-65c80e434f72",
     *         },
     *       ),
     *     ),
     *   ),
     *   @OA\Response(
     *     response="400",
     *     description="Unexpected Error",
     *     @OA\JsonContent(ref="#/components/schemas/default_error_response_content")
     *     ),
     *   ),
     * )
     *
     * @param string $id id
     * @return void
     */
    public function edit(string $id): void
    {
        $this->request->allowMethod(['patch', 'post', 'put']);
        $recipe = $this->Recipes->get($id);
        $recipe = $this->Recipes->patchEntity($recipe, $this->request->getData());
        if ($this->Recipes->save($recipe)) {
            $message = 'Saved';
        } else {
            $message = 'Error';
        }
        $this->set([
            'message' => $message,
            'recipe' => $recipe,
        ]);
        $this->viewBuilder()->setOption('serialize', ['recipe', 'message']);
    }

    /**
     * Delete method
     *
     * @OA\Delete(
     *   path="/api/recipe/delete/{id}.json",
     *   operationId = "RecipeDelete",
     *   tags={"Recipe"},
     *   summary="レシピを削除する",
     *   @OA\Parameter(
     *     name="id",
     *     in="path",
     *     required=true,
     *     description="レシピID",
     *     @OA\Schema(type="string"),
     *     example="c366f5be-360b-45cc-8282-65c80e434f72"
     *   ),
     *   @OA\Response(
     *     response=204,
     *     description="No Content",
     *   ),
     *   @OA\Response(
     *     response="400",
     *     description="Unexpected Error",
     *     @OA\JsonContent(
     *       type="object",
     *       @OA\Property(
     *         property="message",
     *         type="string",
     *         description="エラーメッセージ",
     *       ),
     *      example={
     *        "message"="予期しないエラーです"
     *      },
     *     ),
     *   ),
     * )
     *
     * @param string $id id
     * @return void
     */
    public function delete(string $id): void
    {
        $this->request->allowMethod(['delete']);
        $recipe = $this->Recipes->get($id);
        $message = 'Deleted';
        if (!$this->Recipes->delete($recipe)) {
            $message = 'Error';
        }
        $this->set('message', $message);
        $this->viewBuilder()->setOption('serialize', ['message']);
    }

}
```
## Swagger UIから呼び出せるようにする
Swagger UIからAPIにリクエストしてレスポンスを確認するにはCORSの対応を行う必要があります。
今回のCORS対応は、ローカルで動いているサーバー同士の接続を許可してあげることを意味します。

CORSについてはこちらを参考ください。
[なんとなく CORS がわかる...はもう終わりにする。](https://qiita.com/att55/items/2154a8aad8bf1409db2b)

##### Middlewareでヘッダ情報を付与する
新しく`src/Middleware/CorsMiddleware.php`を作成します。

```src/Middleware/CorsMiddleware.php
<?php
declare(strict_types=1);

namespace App\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class CorsMiddleware implements MiddlewareInterface
{
    /**
     * @param \Psr\Http\Message\ServerRequestInterface $request request
     * @param \Psr\Http\Server\RequestHandlerInterface $handler handler
     * @return \Psr\Http\Message\ResponseInterface
     */
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $response = $handler->handle($request);

        // ローカル開発モードのときのみ CORS に対応する
        if (filter_var(env('SERVER', false))) {
            return $response;
        }

        $response = $response->withHeader('Access-Control-Allow-Origin', '*');
        $response = $response->withHeader('Access-Control-Allow-Methods', '*');
        $response = $response->withHeader('Access-Control-Allow-Headers', 'Content-Type');
        $response = $response->withHeader('Access-Control-Max-Age', '172800');

        return $response;
    }
}

```

#### Preflight request の対応
Controller を作り、Preflight request を処理できるようにします。
参考：[Preflight request (プリフライトリクエスト)](https://developer.mozilla.org/ja/docs/Glossary/Preflight_request)

```src/Controller/Api/CorsController.php
<?php
declare(strict_types=1);

namespace App\Controller\Api;

use App\Controller\AppController;
use Cake\Http\Exception\NotFoundException;

/**
 * CORS Controller
 */
class CorsController extends AppController
{
    /**
     * @return void
     */
    public function options(): void
    {
        // ローカル開発モードのときのみ CORS に対応する
        if (filter_var(env('SERVER', false))) {
            throw new NotFoundException('Not support CORS.');
        }

        $this->viewBuilder()->setOption('serialize', []);
    }
}
```

#### ルーターの設定
作成した`CorsMiddleware`と`CorsController`を設定します。

``` config/route.php
use App\Middleware\CorsMiddleware;
//...
$routes->scope('/api', ['prefix' => 'Api'], function (RouteBuilder $builder) {
    $builder->registerMiddleware('bodies', new BodyParserMiddleware());
    $builder->applyMiddleware('bodies');
    $builder->registerMiddleware('cors', new CorsMiddleware());
    $builder->applyMiddleware('cors');

    $builder->setExtensions(['json']);

    // Preflight request
    $builder->connect('/*', ['controller' => 'Cors', 'action' => 'options'])->setMethods(['OPTIONS']);

//...
```

## DockerイメージにSwagger UIとSwagger Editorを追加する
`docker-compose.yml`を確認いただくと`swagger-editor`と`swagger-ui`がコメントアウトされているかと思いますので、コメントアウトを外してください。

```docker-compose.yml
//...

# Swagger API
  swagger-editor:
    image: swaggerapi/swagger-editor:v3.14.8
    container_name: cake_swagger-editor
    ports:
      - 8001:8080
    volumes:
      - ./server:/tmp
    environment:
      SWAGGER_FILE: /tmp/openapi.json

  swagger-ui:
    image: swaggerapi/swagger-ui
    container_name: cake_swagger-ui
    ports:
      - 8002:8080
    volumes:
      - ./server:/tmp
    environment:
      SWAGGER_JSON: /tmp/openapi.json

//...
```

#### ドキュメントファイルを生成する

```
 docker-compose exec php php /usr/bin/composer openapi
```
Dockerを再起動する

```
make up
```

実行が完了したら、以下のURLでSwagger UIを確認してみましょう
http://localhost:8002/
![スクリーンショット 2021-03-27 20.29.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94596/fafa4ef3-7096-e891-ed6f-979f78f0bf38.png)

皆さんのお手元でもちゃんと動いてくれていますでしょうか？
だいぶ長くなってしまいましたね、お疲れ様でした&#x1f647;

記事中の操作でもっといい方法がありましたら、アドバイスいただけますと幸いです。
また、上手くいかないところなどありましたら、お気軽にコメントください。
可能な範囲で返信させていただきます。100%解決ことは保証できませんが、精一杯一緒に考えさせていただきます。

少しでも皆さまのお役に立てましたら幸いです&#x1f64b;