👻

OpenAPIのドキュメント資料からCursorでAPIサーバーの処理を自動生成してみた

に公開

バックエンドの実装をするときにAPI定義書をOpenAPIの仕様に則って作成することがあるため、このドキュメントの内容次第ではAIに生成してもらうことも可能なんでは?と思い、本稿を執筆。

API定義書

LaravelのPJを生成したときにusersテーブルがデフォルトで生成されるため、一旦usersテーブルのCRUDを実行するAPIを定義書に記載。

api.yaml
openapi: 3.1.0
x-stoplight:
  id: nux3esl6wshjm
info:
  title: api
  version: '1.0'
  description: cursor_testプロジェクトのAPI定義を記します。
servers:
  - url: 'http://localhost:3000'
paths:
  '/users/{userId}':
    parameters:
      - schema:
          type: integer
        name: userId
        in: path
        required: true
        description: usersテーブル idカラム
    get:
      summary: ユーザの詳細取得
      tags: []
      responses:
        '200':
          description: User Found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
                description: passwordの項目だけはレスポンスに含めないこと
              examples:
                Example 1:
                  value:
                    id: 0
                    name: string
                    email: user@example.com
                    email_verified_at: '1997-10-31'
                    password: pa$$word
                    created_at: '2019-08-24T14:15:22Z'
                    updated_at: '2019-08-24T14:15:22Z'
        '404':
          $ref: '#/components/responses/ErrorResponse'
        '500':
          $ref: '#/components/responses/ErrorResponse'
      operationId: get-users-userId
      description: Retrieve the information of the user with the matching user ID.
    put:
      summary: ユーザの更新
      operationId: put-users-userId
      responses:
        '200':
          description: OK
          headers: {}
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
                description: passwordの項目だけはレスポンスに含めないこと
        '422':
          description: Laravelのカスタムリクエストのレスポンスに準拠する
          content:
            application/json:
              schema:
                type: object
                properties: {}
        '500':
          $ref: '#/components/responses/ErrorResponse'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                id:
                  type: integer
                  x-stoplight:
                    id: ce2lqw8rc9ov6
                name:
                  type: string
                  x-stoplight:
                    id: mgu2bovm7o0aw
                email:
                  type: string
                  x-stoplight:
                    id: h8u1fcjfo1k62
                  description: メールアドレスの形式バリデーション
                  format: email
                password:
                  type: string
                  x-stoplight:
                    id: 496gas1lnrpt2
              required:
                - id
                - name
                - email
                - password
            examples:
              Example 1:
                value:
                  id: 0
                  name: string
                  email: user@example.com
                  password: string
        description: |-
          バリデーション必須。各項目ごとに以下のバリデーションルールを組み込むこと。
          ・必須・任意のチェック
          ・型のチェック
          ・descriptionに記載されたルール
    delete:
      summary: ユーザの削除
      operationId: delete-users-userId
      responses:
        '200':
          description: OK
  /users:
    post:
      summary: ユーザの作成
      operationId: post-user
      responses:
        '200':
          description: User Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
                description: passwordの項目だけはレスポンスに含めないこと
              examples:
                New User Bob Fellow:
                  value:
                    id: 12
                    firstName: Bob
                    lastName: Fellow
                    email: bob.fellow@gmail.com
                    dateOfBirth: '1996-08-24'
                    emailVerified: false
                    createDate: '2020-11-18'
        '400':
          description: Missing Required Information
        '409':
          description: Email Already Taken
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  x-stoplight:
                    id: ay6x3z8bga6om
                email:
                  type: string
                  format: email
                  description: メールアドレスの形式バリデーション
                password:
                  type: string
                  format: password
                  x-stoplight:
                    id: qyoegke8bt9xt
              required:
                - name
                - email
                - password
            examples:
              Example 1:
                value:
                  name: string
                  email: user@example.com
                  password: pa$$word
        description: |-
          バリデーション必須。各項目ごとに以下のバリデーションルールを組み込むこと。
          ・必須・任意のチェック
          ・型のチェック
          ・descriptionに記載されたルール
      description: Create a new user.
    parameters: []
    get:
      summary: ユーザの一括取得
      operationId: get-users
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                description: passwordの項目だけはレスポンスに含めないこと
                items:
                  $ref: '#/components/schemas/User'
                  x-stoplight:
                    id: 33y5744t8tp6p
              examples:
                Example 1:
                  value:
                    - id: 0
                      name: string
                      email: user@example.com
                      email_verified_at: '1997-10-31'
                      password: pa$$word
                      created_at: '2019-08-24T14:15:22Z'
                      updated_at: '2019-08-24T14:15:22Z'
        '404':
          $ref: '#/components/responses/ErrorResponse'
        '500':
          $ref: '#/components/responses/ErrorResponse'
      requestBody:
        content: {}
components:
  schemas:
    User:
      title: User
      type: object
      description: usersテーブルの定義を記載しています。
      examples:
        - id: 0
          name: string
          email: user@example.com
          email_verified_at: '1997-10-31'
          password: pa$$word
          created_at: '2019-08-24T14:15:22Z'
          updated_at: '2019-08-24T14:15:22Z'
      properties:
        id:
          type: integer
          description: Unique identifier for the given user.
        name:
          type: string
          x-stoplight:
            id: 2zitq6819v3ke
          description: ユーザ名
        email:
          type: string
          format: email
          description: メールアドレス
        email_verified_at:
          type: string
          format: date-time
          example: '1997-10-31'
          x-stoplight:
            id: 0d35i7du2q0lc
          description: メールアドレスの認証日時
        password:
          type: string
          format: password
          description: The date that the user was created.
          x-stoplight:
            id: 9d06w5p375di5
        created_at:
          type: string
          x-stoplight:
            id: jtazcf7loecxh
          description: laravelのtimestamps
          format: date-time
        updated_at:
          type: string
          x-stoplight:
            id: d5xpidd1uwwmb
          description: laravelのtimestamps
          format: date-time
      required:
        - id
        - name
        - email
        - email_verified_at
        - password
  responses:
    ErrorResponse:
      description: エラーレスポンスの共通化
      content:
        application/json:
          schema:
            type: object
            properties:
              status:
                type: integer
                x-stoplight:
                  id: gcr8hciprl87m
                description: Httpのステータスコード
              title:
                type: string
                x-stoplight:
                  id: fi1cl6jq6a5mp
                description: エラーの概要(LaravelのExceptionクラスに応じてエラーを一言で表現。例:AuthenticationExceptionなら「認証エラーが発生しました」と記載する)
              message:
                type: string
                x-stoplight:
                  id: imj559gk6owky
                description: エラーの詳細メッセージを記載
            required:
              - status
              - title
              - message
          examples:
            Example 1:
              value:
                status: 0
                title: string
                message: string

プロンプト

まず.cursorrulesファイルをプロジェクトのルートディレクトリに配置。
.cursorrulesとはCursorにコード生成するための規約を記載できるファイルとなります。
これを作っておくことで、自動生成されるファイルにその規約が適用されるため、本稿でも活用させていただきます。

また、LaravelがデフォルトでUserモデル等のファイルをプロジェクト生成時に作成してくれているのですでに存在するファイルは作らないように指示をしました。

.cursorrules
# プロジェクト概要
カーソルにOpenAPIの情報を元にAPIサーバーを自動生成してもらうためのプロジェクト

# コーディング規約
- コードのフォーマットは、PSR-12を遵守する
- コードのコメントは全て日本語で記述する
- テーブル作成はマイグレーションファイルを使用してください
- モデル作成はEloquentモデルを使用してください
- コントローラー作成はコントローラーを使用してください
- api作成時のルーティングはapi.phpを使用してください

# アーキテクチャパターン
- Repository層とService層の概念に従ってコードを記述してください
- Repository層: データアクセスロジックを担当(Eloquentモデルとのやり取り)
- Service層: ビジネスロジックを担当(複雑な処理、複数のRepositoryの組み合わせ)
- Controller層: HTTPリクエストの処理のみ(Service層を呼び出す)
- Request層: リクエストのバリデーションを担当(Controller層から呼び出される)
- 各層の責任を明確に分離し、依存関係を適切に管理してください
- 必要に応じて、Interfaceを使用してRepository層を抽象化してください

# フォルダ構成
- Controllerの生成前にHttpフォルダ直下のControllersフォルダ直下にApiフォルダを作成してください(すでに存在する場合は作成しない)
- Serviceクラスの生成前にappフォルダ直下にServiceフォルダを作成してください(すでに存在する場合は作成しない)
- Repositoryクラスの生成前にappフォルダ直下にRepositoryフォルダを作成してください(すでに存在する場合は作成しない)

# 参照ファイル
- api/reference/api.yaml

# 参照ルール
- api.yamlのpathsを参照してAPIのリクエストやパラメータを生成してください
- api.yamlのpathsのdescriptionは生成に関するルールを記載しているので、必ず参照してAPIのリクエストやパラメータの生成に反映してください
- api.yamlのComponentsのschemasを参照してマイグレーションファイルを作成してください(descriptionに記載されているテーブル名を見て、すでにマイグレーションファイルが生成済みの場合はマイグレーションファイルの生成は不要)
- api.yamlのComponentsのschemasを参照してモデルを作成してください(すでに存在する場合はモデルの生成は不要)

# その他
- READEME.mdにプロジェクトの初期化に必須な処理を日本語で記載してください(例:マイグレーションコマンドの実行...など)
- バリデーションメッセージは「https://qiita.com/2024_Hello_World/items/991445da967eba1839c6」を参考に日本語用のバリデーションメッセージ用ファイルを作成してください
  ※ただし、「https://qiita.com/2024_Hello_World/items/991445da967eba1839c6」の「まとめ」を見出しに記載されている箇所については無視をすること。

Cursorには「.cursorrulesファイルに則って、api.ymlの定義に沿ったAPI処理を実装して。」と指示を出して実行。

usersのAPIの実装結果

数分で作成は完了(早い!!)
結果として以下のファイルが生成されました。

  • app/Http/Controllers/Api/UserController.php
  • app/Http/Requests/UserCreateRequest.php
  • app/Http/Requests/UserUpdateRequest.php
  • app/Repository/UserRepositoryInterface.php
  • app/Repository/UserRepository.php
  • app/Service/UserService.php
  • app/Providers/RepositoryServiceProvider.php
  • resources/lang/ja/validation.php

いざGETメソッドでAPIを実行。

http://localhost:8000/api/users
syntax error, unexpected 'UserService' (T_STRING), expecting function (T_FUNCTION) or const (T_CONST)

  at app/Http/Controllers/Api/UserController.php:24
     20▕      * ユーザーサービス
     21▕      *
     22▕      * @var UserService
     23▕      */
  ➜  24▕     private UserService $userService;
     25▕ 
     26▕     /**
     27▕      * コンストラクタ
     28▕      *

      +1 vendor frames 
  2   [internal]:0
      Composer\Autoload\ClassLoader::loadClass("App\Http\Controllers\Api\UserController")

  3   [internal]:0
      spl_autoload_call("App\Http\Controllers\Api\UserController")

クラス変数の部分でエラーが発生していますね。。。
実行した環境がPHP 7.4の環境でして、バージョンが対応していない書き方のためにエラーが出ていますね。「UserService」と型指定していた部分を削除し、再度実行。

Target [App\Repository\UserRepositoryInterface] is not instantiable while building [App\Http\Controllers\Api\UserController, App\Service\UserService].

  at vendor/laravel/framework/src/Illuminate/Container/Container.php:1089
    1085▕         } else {
    1086▕             $message = "Target [$concrete] is not instantiable.";
    1087▕         }
    1088▕ 
  ➜ 1089▕         throw new BindingResolutionException($message);
    1090▕     }
    1091▕ 
    1092▕     /**
    1093▕      * Throw an exception for an unresolvable primitive.

      +26 vendor frames 
  27  [internal]:0
      Illuminate\Foundation\Console\RouteListCommand::Illuminate\Foundation\Console\{closure}(Object(Illuminate\Routing\Route))

      +16 vendor frames 
  44  artisan:37
      Illuminate\Foundation\Console\Kernel::handle(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))

今度はUserRepositoryInterfaceのインスタンス化でこけていますね。。。
インスタンス化の名前解決をapp/Providers/RepositoryServiceProvider.phpファイルで実施しているのですが、config/app.phpファイルにてそのファイルの定義が抜けているためにエラーが起きているみたいです。

config/app.php の修正

'providers' => [
    // ... 既存のプロバイダー ...
    App\Providers\EventServiceProvider::class,
+   App\Providers\RepositoryServiceProvider::class,
    App\Providers\RouteServiceProvider::class,
    // ... その他のプロバイダー ...
],

これで再度実行。

ちゃんとレスポンスが返ってくるようになりました!!

get:
  summary: ユーザの一括取得
  operationId: get-users
  responses:
    '200':
      description: OK
      content:
        application/json:
          schema:
            type: array
            description: passwordの項目だけはレスポンスに含めないこと
            items:
              $ref: '#/components/schemas/User'
              x-stoplight:
                id: 33y5744t8tp6p
          examples:
            Example 1:
              value:
                - id: 0
                  name: string
                  email: user@example.com
                  email_verified_at: '1997-10-31'
                  password: pa$$word
                  created_at: '2019-08-24T14:15:22Z'
                  updated_at: '2019-08-24T14:15:22Z'
    '404':
      $ref: '#/components/responses/ErrorResponse'
    '500':
      $ref: '#/components/responses/ErrorResponse'
  requestBody:
    content: {}

API定義書ではexamplesにpasswordも含めた状態で定義してしまっていたとしてもdescriptionに記載したpasswordをレスポンスに含めないというルールを遵守して実装してくれています!!

※ちなみに記事には記載していないのですが、事前にPOSTメソッドでもAPIを実行してユーザ登録をしてユーザ生成ができることも確認していたため、ユーザは2件存在しています。

また、バリデーションも引っかかるかチェックをするとちゃんとレスポンスが返ってきました。

ユーザ削除も問題ありませんでした。

ちなみにユーザが見つからない場合のエラーレスポンスもAPI定義書に則って返してくれます。

tasksテーブルの生成とそれに関するCRUDの処理を定義

usersテーブルはLaravelのプロジェクト構築時にModelとマイグレーションファイルがすでに生成済みだったので、それらのファイルも1から作成することや、また既存のModelファイルとのリレーションを持たせてAPIを返せるか確認もしてもようと思います。

api.yaml

1. タスクの一括取得

+ /tasks:
+   get:
+     summary: タスクの一括取得
+     tags: []
+     responses:
+       '200':
+         description: OK
+         content:
+           application/json:
+             schema:
+               type: array
+               items:
+                 x-stoplight:
+                   id: 0mrh33ho446ic
+                 type: object
+                 properties:
+                   id:
+                     type: integer
+                     x-stoplight:
+                       id: 45ok2s79itfzy
+                     description: tasksテーブルのid
+                   user_id:
+                     type: integer
+                     x-stoplight:
+                       id: cehp9aprlx5v3
+                     description: tasksテーブルのuser_id
+                   title:
+                     type: string
+                     x-stoplight:
+                       id: 8el0tvtih2928
+                     description: tasksテーブルのtitle
+                   detail:
+                     type: string
+                     x-stoplight:
+                       id: leq2f2dve20fq
+                     description: tasksテーブルのdetail
+                   user:
+                     $ref: '#/components/schemas/User'
+                     x-stoplight:
+                       id: nujcp26u3030d
+                     description: usersテーブルの値をLaravelのEloquentからリレーションとEager Loadingの機能を使用して定義する
+                 required:
+                   - id
+                   - user_id
+                   - title
+                   - detail
+                   - user
+             examples:
+               Example 1:
+                 value:
+                   - id: 0
+                     user_id: 0
+                     title: string
+                     detail: string
+                     user:
+                       id: 0
+                       name: string
+                       email: user@example.com
+                       email_verified_at: '1997-10-31'
+                       password: pa$$word
+                       created_at: '2019-08-24T14:15:22Z'
+                       updated_at: '2019-08-24T14:15:22Z'
+       '404':
+         $ref: '#/components/responses/ErrorResponse'
+       '500':
+         $ref: '#/components/responses/ErrorResponse'
+     operationId: get-tasks
+     requestBody:
+       content:
+         application/json:
+           schema:
+             type: object
+             properties: {}

2. タスクの作成

+   post:
+     summary: タスクの作成
+     operationId: post-tasks
+     responses:
+       '200':
+         description: |-
+           バリデーション必須。各項目ごとに以下のバリデーションルールを組み込むこと。
+           ・必須・任意のチェック
+           ・型のチェック
+           ・descriptionに記載されたルール
+         content:
+           application/json:
+             schema:
+               type: object
+               properties:
+                 id:
+                   type: integer
+                   x-stoplight:
+                     id: 45ok2s79itfzy
+                   description: tasksテーブルのid
+                 user_id:
+                   type: integer
+                   x-stoplight:
+                     id: cehp9aprlx5v3
+                   description: tasksテーブルのuser_id
+                 title:
+                   type: string
+                   x-stoplight:
+                     id: 8el0tvtih2928
+                   description: tasksテーブルのtitle
+                 detail:
+                   type: string
+                   x-stoplight:
+                     id: leq2f2dve20fq
+                   description: tasksテーブルのdetail
+                 user:
+                   $ref: '#/components/schemas/User'
+                   x-stoplight:
+                     id: nujcp26u3030d
+                   description: usersテーブルの値をLaravelのEloquentからリレーションとEager Loadingの機能を使用して定義する
+               required:
+                 - id
+                 - user_id
+                 - title
+                 - detail
+                 - user
+             examples:
+               Example 1:
+                 value:
+                   id: 0
+                   user_id: 0
+                   title: string
+                   detail: string
+                   user:
+                     id: 0
+                     name: string
+                     email: user@example.com
+                     email_verified_at: '1997-10-31'
+                     password: pa$$word
+                     created_at: '2019-08-24T14:15:22Z'
+                     updated_at: '2019-08-24T14:15:22Z'
+       '422':
+         description: Laravelのカスタムリクエストのレスポンスに準拠する
+       '500':
+         $ref: '#/components/responses/ErrorResponse'
+     requestBody:
+       content:
+         application/json:
+           schema:
+             type: object
+             properties:
+               user_id:
+                 type: integer
+                 x-stoplight:
+                   id: 0gkre7151vryk
+                 description: usersテーブルに存在するidであることをバリデーションチェック
+               title:
+                 type: string
+                 x-stoplight:
+                   id: ehx8fl6q5d53f
+               detail:
+                 type: string
+                 x-stoplight:
+                   id: x0t4vcv82x409
+             required:
+               - user_id
+               - title
+               - detail
+           examples:
+             Example 1:
+               value:
+                 user_id: 0
+                 title: string
+                 detail: string
+       description: |-
+         バリデーション必須。各項目ごとに以下のバリデーションルールを組み込むこと。
+         ・必須・任意のチェック
+         ・型のチェック
+         ・descriptionに記載されたルール

3. タスクの詳細取得

+ '/tasks/{taskId}':
+   parameters:
+     - schema:
+         type: string
+       name: taskId
+       in: path
+       required: true
+       description: tasksテーブル idカラム
+   get:
+     summary: タスクの詳細取得
+     tags: []
+     responses:
+       '200':
+         description: User Found
+         content:
+           application/json:
+             schema:
+               type: object
+               properties:
+                 id:
+                   type: integer
+                   x-stoplight:
+                     id: 45ok2s79itfzy
+                   description: tasksテーブルのid
+                 user_id:
+                   type: integer
+                   x-stoplight:
+                     id: cehp9aprlx5v3
+                   description: tasksテーブルのuser_id
+                 title:
+                   type: string
+                   x-stoplight:
+                     id: 8el0tvtih2928
+                   description: tasksテーブルのtitle
+                 detail:
+                   type: string
+                   x-stoplight:
+                     id: leq2f2dve20fq
+                   description: tasksテーブルのdetail
+                 user:
+                   $ref: '#/components/schemas/User'
+                   x-stoplight:
+                     id: nujcp26u3030d
+                   description: usersテーブルの値をLaravelのEloquentからリレーションとEager Loadingの機能を使用して定義する
+               required:
+                 - id
+                 - user_id
+                 - title
+                 - detail
+                 - user
+             examples:
+               Example 1:
+                 value:
+                   id: 0
+                   user_id: 0
+                   title: string
+                   detail: string
+                   user:
+                     id: 0
+                     name: string
+                     email: user@example.com
+                     email_verified_at: '1997-10-31'
+                     password: pa$$word
+                     created_at: '2019-08-24T14:15:22Z'
+                     updated_at: '2019-08-24T14:15:22Z'
+       '404':
+         $ref: '#/components/responses/ErrorResponse'
+       '500':
+         $ref: '#/components/responses/ErrorResponse'
+     operationId: get-tasks-taskId
+     description: Retrieve the information of the user with the matching user ID.

4. タスクの更新

+   put:
+     summary: タスクの更新
+     operationId: put-tasks-taskId
+     responses:
+       '200':
+         description: OK
+         headers: {}
+         content:
+           application/json:
+             schema:
+               schema: null
+               type: object
+               properties:
+                 id:
+                   type: integer
+                   x-stoplight:
+                     id: 45ok2s79itfzy
+                   description: tasksテーブルのid
+                 user_id:
+                   type: integer
+                   x-stoplight:
+                     id: cehp9aprlx5v3
+                   description: tasksテーブルのuser_id
+                 title:
+                   type: string
+                   x-stoplight:
+                     id: 8el0tvtih2928
+                   description: tasksテーブルのtitle
+                 detail:
+                   type: string
+                   x-stoplight:
+                     id: leq2f2dve20fq
+                   description: tasksテーブルのdetail
+                 user:
+                   $ref: '#/components/schemas/User'
+                   x-stoplight:
+                     id: nujcp26u3030d
+                   description: usersテーブルの値をLaravelのEloquentからリレーションとEager Loadingの機能を使用して定義する
+               required:
+                 - id
+                 - user_id
+                 - title
+                 - detail
+                 - user
+             examples:
+               Example 1:
+                 value:
+                   id: 0
+                   user_id: 0
+                   title: string
+                   detail: string
+                   user:
+                     id: 0
+                     name: string
+                     email: user@example.com
+                     email_verified_at: '1997-10-31'
+                     password: pa$$word
+                     created_at: '2019-08-24T14:15:22Z'
+                     updated_at: '2019-08-24T14:15:22Z'
+       '422':
+         description: Laravelのカスタムリクエストのレスポンスに準拠する
+       '500':
+         $ref: '#/components/responses/ErrorResponse'
+     requestBody:
+       content:
+         application/json:
+           schema:
+             type: object
+             properties:
+               id:
+                 type: string
+                 x-stoplight:
+                   id: i7fmlg4pj8pa0
+                 description: tasksテーブルに存在するidであることをバリデーションチェック。
+               user_id:
+                 type: integer
+                 x-stoplight:
+                   id: 0gkre7151vryk
+                 description: usersテーブルに存在するidであることをバリデーションチェック
+               title:
+                 type: string
+                 x-stoplight:
+                   id: ehx8fl6q5d53f
+               detail:
+                 type: string
+                 x-stoplight:
+                   id: x0t4vcv82x409
+             required:
+               - id
+               - user_id
+               - title
+               - detail
+           examples:
+             Example 1:
+               value:
+                 id: string
+                 user_id: 0
+                 title: string
+                 detail: string
+       description: |-
+         バリデーション必須。各項目ごとに以下のバリデーションルールを組み込むこと。
+         ・必須・任意のチェック
+         ・型のチェック
+         ・descriptionに記載されたルール

5. タスクの削除

+   delete:
+     summary: タスクの削除
+     operationId: delete-tasks-taskId
+     responses:
+       '200':
+         description: OK
+       '404':
+         $ref: '#/components/responses/ErrorResponse'
+       '500':
+         $ref: '#/components/responses/ErrorResponse'

6. タスクのスキーマ

+   Task:
+     title: Task
+     x-stoplight:
+       id: k75z7bud9cybr
+     type: object
+     description: tasksテーブルの定義を記載しています。
+     examples:
+       - id: 0
+         user_id: 0
+         title: string
+         detail: string
+         created_at: '2019-08-24T14:15:22Z'
+         updated_at: '2019-08-24T14:15:22Z'
+     properties:
+       id:
+         type: integer
+         description: id
+       user_id:
+         type: integer
+         x-stoplight:
+           id: qaks1jk3icp0x
+         description: usersテーブルのid
+       title:
+         type: string
+         x-stoplight:
+           id: 0hzkel6aw4lg0
+         description: タスクのタイトル(text型でカラム定義してください)
+       detail:
+         type: string
+         x-stoplight:
+           id: 5odblu2zarz3a
+         description: タスクの詳細(text型でカラム定義してください)
+       created_at:
+         type: string
+         x-stoplight:
+           id: pe1dt70m49ew4
+         format: date-time
+         description: laravelのtimestamps
+       updated_at:
+         type: string
+         x-stoplight:
+           id: c9v4wpn4dbmkf
+         format: date-time
+         description: laravelのtimestamps
+     required:
+       - id
+       - user_id
+       - title
+       - detail

api.yamlにtasksのパスとスキーマを追加します。

.cursorrulesファイルに先ほどの反省点を活かして以下を追記します。

.cursorrules
# コーディング規約
- - コードのフォーマットは、PSR-12を遵守する
+ - コードのフォーマットは、PSR-12を遵守する(ただしphpバージョンは7.4を基準とした記述方法を遵守する)
~ 省略
- バリデーションメッセージは「https://qiita.com/2024_Hello_World/items/991445da967eba1839c6」を参考に日本語用のバリデーションメッセージ用ファイルを作成してください
  ※ただし、「https://qiita.com/2024_Hello_World/items/991445da967eba1839c6」の「まとめ」を見出しに記載されている箇所については無視をすること。
+ - バリデーションメッセージのmessagesメソッドはまずresources/lang/ja/validation.phpを参照してください。そこにないバリデーションの場合にのみメッセージ用のコードを生成してください。

tasksのAPIの実装結果

以下のファイルの作成と更新が実行されました。

新規作成したファイル

  • app/Models/Task.php
  • app/Repository/TaskRepositoryInterface.php
  • app/Repository/TaskRepository.php
  • app/Service/TaskService.php
  • app/Http/Requests/TaskCreateRequest.php
  • app/Http/Requests/TaskUpdateRequest.php
  • app/Http/Controllers/Api/TaskController.php
  • database/migrations/2025_09_24_120043_create_tasks_table.php

更新したファイル

  • app/Models/User.php
  • routes/api.php
  • app/Providers/RepositoryServiceProvider.php

マイグレーションファイルも新規生成されているし、更新ファイルとかは既存のModelにリレーションも追加してくれています!

User.php
     protected $casts = [
         'email_verified_at' => 'datetime',
     ];
 
+    /**
+     * タスクとのリレーション
+     *
+     * @return HasMany
+     */
+    public function tasks(): HasMany
+    {
+        return $this->hasMany(Task::class);
+    }
 }

いざPOSTメソッドで新規生成のAPIを実行。

http://localhost:8000/api/tasks

正常に動作しました!!
しかもレスポンスにはリレーションもしっかり定義されています!!

もちろんGETメソッドで実行するとちゃんとレスポンスが返ってきます。

API定義書にリレーションに関する指示も記載したらちゃんとその通りに実装してくれるのは優秀ですね。

結論

OpenAPIの定義ファイルに細かい指示を書けば、ほぼAI単独の力で十分動作するAPIサーバーを実装できることがわかりました。

ただ、validation.phpにてメッセージを定義しているのに以下のmessagesメソッドに重複して日本語メッセージが記載されているように、一部は.cursorrulesに記載しても守ってくれない部分も見受けられます。(指示の仕方が悪かったのかも。)

TaskCreateRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

/**
 * タスク作成リクエストクラス
 */
class TaskCreateRequest extends FormRequest
{
    /**
     * リクエストの認可を決定
     *
     * @return bool
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * バリデーションルール
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'user_id' => 'required|integer|exists:users,id',
            'title' => 'required|string',
            'detail' => 'required|string',
        ];
    }

    /**
     * バリデーションメッセージ
     *
     * @return array<string, string>
     */
    public function messages(): array
    {
        return [
            'user_id.required' => 'ユーザーIDは必須です。',
            'user_id.integer' => 'ユーザーIDは整数である必要があります。',
            'user_id.exists' => '指定されたユーザーが存在しません。',
            'title.required' => 'タイトルは必須です。',
            'title.string' => 'タイトルは文字列である必要があります。',
            'detail.required' => '詳細は必須です。',
            'detail.string' => '詳細は文字列である必要があります。',
        ];
    }
}

今回実証してみて、可能なこと、注意すべき点は以下にまとめます。

可能なこと

  • レスポンス形式の指定: 指定した形式通りに実装可能
  • テーブル定義・Model作成: OpenAPIのSchemaから自動生成可能
  • カスタムバリデーション: OpenAPIから作成可能
  • バリデーションルール指定: OpenAPIから詳細なルール指定可能

注意すべきなこと

  • 開発環境設定やPHPバージョンを.cursorrulesに記載必須
  • Providerファイルの作成やミドルウェアの作成など設定ファイルに追記しないといけないファイルには新規ファイルの生成時に設定追加することも.cursorrulesに記載が必要
  • カスタムリクエストにて定義するバリデーションメッセージの日本語定義はまずvalidation.phpをチェックさせて、そこにないルールのみ追記するように.cursorrulesに指定が必要

作成・更新されたファイル

最後に今回AIが自動生成や更新をしたファイルを以下に共有しておきますね。

api.php
<?php

use App\Http\Controllers\Api\UserController;
use App\Http\Controllers\Api\TaskController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

// ユーザーAPIルート
Route::apiResource('users', UserController::class);

// タスクAPIルート
Route::apiResource('tasks', TaskController::class);

UserController.php
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\UserCreateRequest;
use App\Http\Requests\UserUpdateRequest;
use App\Service\UserService;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Exception;

/**
 * ユーザーAPIコントローラー
 */
class UserController extends Controller
{
    /**
     * ユーザーサービス
     *
     * @var UserService
     */
    private $userService;

    /**
     * コンストラクタ
     *
     * @param UserService $userService
     */
    public function __construct(UserService $userService)
    {
        $this->userService = $userService;
    }

    /**
     * ユーザー一覧取得
     *
     * @return JsonResponse
     */
    public function index(): JsonResponse
    {
        try {
            $users = $this->userService->getAllUsers();
            return response()->json($users, 200);
        } catch (Exception $e) {
            return $this->errorResponse(500, 'サーバーエラーが発生しました', $e->getMessage());
        }
    }

    /**
     * ユーザー詳細取得
     *
     * @param int $userId
     * @return JsonResponse
     */
    public function show(int $userId): JsonResponse
    {
        try {
            $user = $this->userService->getUserById($userId);
            
            if (!$user) {
                return $this->errorResponse(404, 'ユーザーが見つかりません', '指定されたユーザーが存在しません。');
            }

            return response()->json($user, 200);
        } catch (Exception $e) {
            return $this->errorResponse(500, 'サーバーエラーが発生しました', $e->getMessage());
        }
    }

    /**
     * ユーザー作成
     *
     * @param UserCreateRequest $request
     * @return JsonResponse
     */
    public function store(UserCreateRequest $request): JsonResponse
    {
        try {
            $user = $this->userService->createUser($request->validated());
            return response()->json($user, 200);
        } catch (Exception $e) {
            if ($e->getMessage() === 'このメールアドレスは既に使用されています。') {
                return $this->errorResponse(409, 'メールアドレスが既に使用されています', $e->getMessage());
            }
            return $this->errorResponse(500, 'サーバーエラーが発生しました', $e->getMessage());
        }
    }

    /**
     * ユーザー更新
     *
     * @param UserUpdateRequest $request
     * @param int $userId
     * @return JsonResponse
     */
    public function update(UserUpdateRequest $request, int $userId): JsonResponse
    {
        try {
            $data = $request->validated();
            $user = $this->userService->updateUser($userId, $data);
            
            if (!$user) {
                return $this->errorResponse(404, 'ユーザーが見つかりません', '指定されたユーザーが存在しません。');
            }

            return response()->json($user, 200);
        } catch (Exception $e) {
            if ($e->getMessage() === 'このメールアドレスは既に使用されています。') {
                return $this->errorResponse(422, 'バリデーションエラー', $e->getMessage());
            }
            return $this->errorResponse(500, 'サーバーエラーが発生しました', $e->getMessage());
        }
    }

    /**
     * ユーザー削除
     *
     * @param int $userId
     * @return JsonResponse
     */
    public function destroy(int $userId): JsonResponse
    {
        try {
            $result = $this->userService->deleteUser($userId);
            
            if (!$result) {
                return $this->errorResponse(404, 'ユーザーが見つかりません', '指定されたユーザーが存在しません。');
            }

            return response()->json(['message' => 'ユーザーが正常に削除されました。'], 200);
        } catch (Exception $e) {
            return $this->errorResponse(500, 'サーバーエラーが発生しました', $e->getMessage());
        }
    }

    /**
     * エラーレスポンスを生成
     *
     * @param int $status
     * @param string $title
     * @param string $message
     * @return JsonResponse
     */
    private function errorResponse(int $status, string $title, string $message): JsonResponse
    {
        return response()->json([
            'status' => $status,
            'title' => $title,
            'message' => $message,
        ], $status);
    }
}
TaskController.php
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\TaskCreateRequest;
use App\Http\Requests\TaskUpdateRequest;
use App\Service\TaskService;
use App\Models\Task;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Exception;

/**
 * タスクAPIコントローラー
 */
class TaskController extends Controller
{
    /**
     * タスクサービス
     *
     * @var TaskService
     */
    private $taskService;

    /**
     * コンストラクタ
     *
     * @param TaskService $taskService
     */
    public function __construct(TaskService $taskService)
    {
        $this->taskService = $taskService;
    }

    /**
     * タスク一覧取得
     *
     * @return JsonResponse
     */
    public function index(): JsonResponse
    {
        try {
            $tasks = $this->taskService->getAllTasks();
            return response()->json($tasks, 200);
        } catch (Exception $e) {
            return $this->errorResponse(500, 'サーバーエラーが発生しました', $e->getMessage());
        }
    }

    /**
     * タスク詳細取得
     *
     * @param string $taskId
     * @return JsonResponse
     */
    public function show(string $taskId): JsonResponse
    {
        try {
            $task = $this->taskService->getTaskById((int)$taskId);
            
            if (!$task) {
                return $this->errorResponse(404, 'タスクが見つかりません', '指定されたタスクが存在しません。');
            }

            return response()->json($task, 200);
        } catch (Exception $e) {
            return $this->errorResponse(500, 'サーバーエラーが発生しました', $e->getMessage());
        }
    }

    /**
     * タスク作成
     *
     * @param TaskCreateRequest $request
     * @return JsonResponse
     */
    public function store(TaskCreateRequest $request): JsonResponse
    {
        try {
            $task = $this->taskService->createTask($request->validated());
            return response()->json($task, 200);
        } catch (Exception $e) {
            if ($e->getMessage() === '指定されたユーザーが存在しません。') {
                return $this->errorResponse(422, 'バリデーションエラー', $e->getMessage());
            }
            return $this->errorResponse(500, 'サーバーエラーが発生しました', $e->getMessage());
        }
    }

    /**
     * タスク更新
     *
     * @param TaskUpdateRequest $request
     * @param string $taskId
     * @return JsonResponse
     */
    public function update(TaskUpdateRequest $request, string $taskId): JsonResponse
    {
        try {
            $data = $request->validated();
            $task = $this->taskService->updateTask((int)$taskId, $data);
            
            if (!$task) {
                return $this->errorResponse(404, 'タスクが見つかりません', '指定されたタスクが存在しません。');
            }

            return response()->json($task, 200);
        } catch (Exception $e) {
            if ($e->getMessage() === '指定されたユーザーが存在しません。') {
                return $this->errorResponse(422, 'バリデーションエラー', $e->getMessage());
            }
            return $this->errorResponse(500, 'サーバーエラーが発生しました', $e->getMessage());
        }
    }

    /**
     * タスク削除
     *
     * @param string $taskId
     * @return JsonResponse
     */
    public function destroy(string $taskId): JsonResponse
    {
        try {
            $result = $this->taskService->deleteTask((int)$taskId);
            
            if (!$result) {
                return $this->errorResponse(404, 'タスクが見つかりません', '指定されたタスクが存在しません。');
            }

            return response()->json(['message' => 'タスクが正常に削除されました。'], 200);
        } catch (Exception $e) {
            return $this->errorResponse(500, 'サーバーエラーが発生しました', $e->getMessage());
        }
    }

    /**
     * エラーレスポンスを生成
     *
     * @param int $status
     * @param string $title
     * @param string $message
     * @return JsonResponse
     */
    private function errorResponse(int $status, string $title, string $message): JsonResponse
    {
        return response()->json([
            'status' => $status,
            'title' => $title,
            'message' => $message,
        ], $status);
    }
}
UserCreateRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

/**
 * ユーザー作成リクエストクラス
 */
class UserCreateRequest extends FormRequest
{
    /**
     * リクエストの認可を決定
     *
     * @return bool
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * バリデーションルール
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users,email|max:255',
            'password' => 'required|string|min:8',
        ];
    }

    /**
     * バリデーションメッセージ
     *
     * @return array<string, string>
     */
    public function messages(): array
    {
        return [
            'name.required' => 'ユーザー名は必須です。',
            'name.string' => 'ユーザー名は文字列である必要があります。',
            'name.max' => 'ユーザー名は255文字以内で入力してください。',
            'email.required' => 'メールアドレスは必須です。',
            'email.email' => '有効なメールアドレスを入力してください。',
            'email.unique' => 'このメールアドレスは既に使用されています。',
            'email.max' => 'メールアドレスは255文字以内で入力してください。',
            'password.required' => 'パスワードは必須です。',
            'password.string' => 'パスワードは文字列である必要があります。',
            'password.min' => 'パスワードは8文字以上で入力してください。',
        ];
    }
}

UserUpdateRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

/**
 * ユーザー更新リクエストクラス
 */
class UserUpdateRequest extends FormRequest
{
    /**
     * リクエストの認可を決定
     *
     * @return bool
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * バリデーションルール
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        $userId = $this->route('userId');
        
        return [
            'id' => 'required|integer|exists:users,id',
            'name' => 'required|string|max:255',
            'email' => [
                'required',
                'email',
                'max:255',
                Rule::unique('users', 'email')->ignore($userId)
            ],
            'password' => 'required|string|min:8',
        ];
    }

    /**
     * バリデーションメッセージ
     *
     * @return array<string, string>
     */
    public function messages(): array
    {
        return [
            'id.required' => 'IDは必須です。',
            'id.integer' => 'IDは整数である必要があります。',
            'id.exists' => '指定されたユーザーが存在しません。',
            'name.required' => 'ユーザー名は必須です。',
            'name.string' => 'ユーザー名は文字列である必要があります。',
            'name.max' => 'ユーザー名は255文字以内で入力してください。',
            'email.required' => 'メールアドレスは必須です。',
            'email.email' => '有効なメールアドレスを入力してください。',
            'email.unique' => 'このメールアドレスは既に使用されています。',
            'email.max' => 'メールアドレスは255文字以内で入力してください。',
            'password.required' => 'パスワードは必須です。',
            'password.string' => 'パスワードは文字列である必要があります。',
            'password.min' => 'パスワードは8文字以上で入力してください。',
        ];
    }
}

TaskCreateRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

/**
 * タスク作成リクエストクラス
 */
class TaskCreateRequest extends FormRequest
{
    /**
     * リクエストの認可を決定
     *
     * @return bool
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * バリデーションルール
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'user_id' => 'required|integer|exists:users,id',
            'title' => 'required|string',
            'detail' => 'required|string',
        ];
    }

    /**
     * バリデーションメッセージ
     *
     * @return array<string, string>
     */
    public function messages(): array
    {
        return [
            'user_id.required' => 'ユーザーIDは必須です。',
            'user_id.integer' => 'ユーザーIDは整数である必要があります。',
            'user_id.exists' => '指定されたユーザーが存在しません。',
            'title.required' => 'タイトルは必須です。',
            'title.string' => 'タイトルは文字列である必要があります。',
            'detail.required' => '詳細は必須です。',
            'detail.string' => '詳細は文字列である必要があります。',
        ];
    }
}
TaskUpdateRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

/**
 * タスク更新リクエストクラス
 */
class TaskUpdateRequest extends FormRequest
{
    /**
     * リクエストの認可を決定
     *
     * @return bool
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * バリデーションルール
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'id' => 'required|string|exists:tasks,id',
            'user_id' => 'required|integer|exists:users,id',
            'title' => 'required|string',
            'detail' => 'required|string',
        ];
    }

    /**
     * バリデーションメッセージ
     *
     * @return array<string, string>
     */
    public function messages(): array
    {
        return [
            'id.required' => 'IDは必須です。',
            'id.string' => 'IDは文字列である必要があります。',
            'id.exists' => '指定されたタスクが存在しません。',
            'user_id.required' => 'ユーザーIDは必須です。',
            'user_id.integer' => 'ユーザーIDは整数である必要があります。',
            'user_id.exists' => '指定されたユーザーが存在しません。',
            'title.required' => 'タイトルは必須です。',
            'title.string' => 'タイトルは文字列である必要があります。',
            'detail.required' => '詳細は必須です。',
            'detail.string' => '詳細は文字列である必要があります。',
        ];
    }
}

User.php
<?php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    /**
     * 一括代入可能な属性
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
        'email_verified_at',
    ];

    /**
     * シリアライゼーション時に非表示にする属性
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * 型変換する属性
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    /**
     * タスクとのリレーション
     *
     * @return HasMany
     */
    public function tasks(): HasMany
    {
        return $this->hasMany(Task::class);
    }
}

Task.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Task extends Model
{
    use HasFactory;

    /**
     * 一括代入可能な属性
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'user_id',
        'title',
        'detail',
    ];

    /**
     * 型変換する属性
     *
     * @var array<string, string>
     */
    protected $casts = [
        'created_at' => 'datetime',
        'updated_at' => 'datetime',
    ];

    /**
     * ユーザーとのリレーション
     *
     * @return BelongsTo
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

RepositoryServiceProvider.php
<?php

namespace App\Providers;

use App\Repository\UserRepositoryInterface;
use App\Repository\UserRepository;
use App\Repository\TaskRepositoryInterface;
use App\Repository\TaskRepository;
use Illuminate\Support\ServiceProvider;

/**
 * リポジトリサービスプロバイダー
 */
class RepositoryServiceProvider extends ServiceProvider
{
    /**
     * サービスを登録
     *
     * @return void
     */
    public function register(): void
    {
        $this->app->bind(UserRepositoryInterface::class, UserRepository::class);
        $this->app->bind(TaskRepositoryInterface::class, TaskRepository::class);
    }

    /**
     * サービスを起動
     *
     * @return void
     */
    public function boot(): void
    {
        //
    }
}

UserRepositoryInterface.php
<?php

namespace App\Repository;

use App\Models\User;

/**
 * ユーザーリポジトリインターフェース
 */
interface UserRepositoryInterface
{
    /**
     * 全ユーザーを取得
     *
     * @return \Illuminate\Database\Eloquent\Collection
     */
    public function getAll();

    /**
     * IDでユーザーを取得
     *
     * @param int $id
     * @return User|null
     */
    public function findById(int $id): ?User;

    /**
     * ユーザーを作成
     *
     * @param array $data
     * @return User
     */
    public function create(array $data): User;

    /**
     * ユーザーを更新
     *
     * @param int $id
     * @param array $data
     * @return User|null
     */
    public function update(int $id, array $data): ?User;

    /**
     * ユーザーを削除
     *
     * @param int $id
     * @return bool
     */
    public function delete(int $id): bool;

    /**
     * メールアドレスでユーザーを検索
     *
     * @param string $email
     * @return User|null
     */
    public function findByEmail(string $email): ?User;
}

UserRepository.php
<?php

namespace App\Repository;

use App\Models\User;

/**
 * ユーザーリポジトリ実装クラス
 */
class UserRepository implements UserRepositoryInterface
{
    /**
     * 全ユーザーを取得
     *
     * @return \Illuminate\Database\Eloquent\Collection
     */
    public function getAll()
    {
        return User::all();
    }

    /**
     * IDでユーザーを取得
     *
     * @param int $id
     * @return User|null
     */
    public function findById(int $id): ?User
    {
        return User::find($id);
    }

    /**
     * ユーザーを作成
     *
     * @param array $data
     * @return User
     */
    public function create(array $data): User
    {
        return User::create($data);
    }

    /**
     * ユーザーを更新
     *
     * @param int $id
     * @param array $data
     * @return User|null
     */
    public function update(int $id, array $data): ?User
    {
        $user = User::find($id);
        if ($user) {
            $user->update($data);
            return $user->fresh();
        }
        return null;
    }

    /**
     * ユーザーを削除
     *
     * @param int $id
     * @return bool
     */
    public function delete(int $id): bool
    {
        $user = User::find($id);
        if ($user) {
            return $user->delete();
        }
        return false;
    }

    /**
     * メールアドレスでユーザーを検索
     *
     * @param string $email
     * @return User|null
     */
    public function findByEmail(string $email): ?User
    {
        return User::where('email', $email)->first();
    }
}
TaskRepositoryInterface.php
<?php

namespace App\Repository;

use App\Models\Task;
use Illuminate\Database\Eloquent\Collection;

/**
 * タスクリポジトリインターフェース
 */
interface TaskRepositoryInterface
{
    /**
     * 全タスクを取得(ユーザー情報も含む)
     *
     * @return Collection
     */
    public function getAll(): Collection;

    /**
     * IDでタスクを取得(ユーザー情報も含む)
     *
     * @param int $id
     * @return Task|null
     */
    public function findById(int $id): ?Task;

    /**
     * タスクを作成
     *
     * @param array $data
     * @return Task
     */
    public function create(array $data): Task;

    /**
     * タスクを更新
     *
     * @param int $id
     * @param array $data
     * @return Task|null
     */
    public function update(int $id, array $data): ?Task;

    /**
     * タスクを削除
     *
     * @param int $id
     * @return bool
     */
    public function delete(int $id): bool;

    /**
     * ユーザーIDでタスクを検索
     *
     * @param int $userId
     * @return Collection
     */
    public function findByUserId(int $userId): Collection;
}

TaskRepository.php
<?php

namespace App\Repository;

use App\Models\Task;

/**
 * タスクリポジトリ実装クラス
 */
class TaskRepository implements TaskRepositoryInterface
{
    /**
     * 全タスクを取得(ユーザー情報も含む)
     *
     * @return \Illuminate\Database\Eloquent\Collection
     */
    public function getAll(): \Illuminate\Database\Eloquent\Collection
    {
        return Task::with('user')->get();
    }

    /**
     * IDでタスクを取得(ユーザー情報も含む)
     *
     * @param int $id
     * @return Task|null
     */
    public function findById(int $id): ?Task
    {
        return Task::with('user')->find($id);
    }

    /**
     * タスクを作成
     *
     * @param array $data
     * @return Task
     */
    public function create(array $data): Task
    {
        return Task::create($data);
    }

    /**
     * タスクを更新
     *
     * @param int $id
     * @param array $data
     * @return Task|null
     */
    public function update(int $id, array $data): ?Task
    {
        $task = Task::find($id);
        if ($task) {
            $task->update($data);
            return $task->fresh(['user']);
        }
        return null;
    }

    /**
     * タスクを削除
     *
     * @param int $id
     * @return bool
     */
    public function delete(int $id): bool
    {
        $task = Task::find($id);
        if ($task) {
            return $task->delete();
        }
        return false;
    }

    /**
     * ユーザーIDでタスクを検索
     *
     * @param int $userId
     * @return \Illuminate\Database\Eloquent\Collection
     */
    public function findByUserId(int $userId): \Illuminate\Database\Eloquent\Collection
    {
        return Task::with('user')->where('user_id', $userId)->get();
    }
}

UserService.php
<?php

namespace App\Service;

use App\Models\User;
use App\Repository\UserRepositoryInterface;
use Illuminate\Database\Eloquent\Collection;

/**
 * ユーザーサービスクラス
 */
class UserService
{
    /**
     * ユーザーリポジトリ
     *
     * @var UserRepositoryInterface
     */
    private $userRepository;

    /**
     * コンストラクタ
     *
     * @param UserRepositoryInterface $userRepository
     */
    public function __construct(UserRepositoryInterface $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    /**
     * 全ユーザーを取得
     *
     * @return Collection
     */
    public function getAllUsers(): Collection
    {
        return $this->userRepository->getAll();
    }

    /**
     * IDでユーザーを取得
     *
     * @param int $id
     * @return User|null
     */
    public function getUserById(int $id): ?User
    {
        return $this->userRepository->findById($id);
    }

    /**
     * ユーザーを作成
     *
     * @param array $data
     * @return User
     * @throws \Exception
     */
    public function createUser(array $data): User
    {
        // メールアドレスの重複チェック
        if ($this->userRepository->findByEmail($data['email'])) {
            throw new \Exception('このメールアドレスは既に使用されています。');
        }

        // パスワードをハッシュ化
        $data['password'] = bcrypt($data['password']);

        return $this->userRepository->create($data);
    }

    /**
     * ユーザーを更新
     *
     * @param int $id
     * @param array $data
     * @return User|null
     * @throws \Exception
     */
    public function updateUser(int $id, array $data): ?User
    {
        $user = $this->userRepository->findById($id);
        if (!$user) {
            return null;
        }

        // メールアドレスの重複チェック(自分以外)
        if (isset($data['email']) && $data['email'] !== $user->email) {
            $existingUser = $this->userRepository->findByEmail($data['email']);
            if ($existingUser && $existingUser->id !== $id) {
                throw new \Exception('このメールアドレスは既に使用されています。');
            }
        }

        // パスワードが含まれている場合はハッシュ化
        if (isset($data['password'])) {
            $data['password'] = bcrypt($data['password']);
        }

        return $this->userRepository->update($id, $data);
    }

    /**
     * ユーザーを削除
     *
     * @param int $id
     * @return bool
     */
    public function deleteUser(int $id): bool
    {
        return $this->userRepository->delete($id);
    }
}

TaskService.php
<?php

namespace App\Service;

use App\Models\Task;
use App\Models\User;
use App\Repository\TaskRepositoryInterface;
use App\Repository\UserRepositoryInterface;
use Illuminate\Database\Eloquent\Collection;

/**
 * タスクサービスクラス
 */
class TaskService
{
    /**
     * タスクリポジトリ
     *
     * @var TaskRepositoryInterface
     */
    private $taskRepository;

    /**
     * ユーザーリポジトリ
     *
     * @var UserRepositoryInterface
     */
    private $userRepository;

    /**
     * コンストラクタ
     *
     * @param TaskRepositoryInterface $taskRepository
     * @param UserRepositoryInterface $userRepository
     */
    public function __construct(TaskRepositoryInterface $taskRepository, UserRepositoryInterface $userRepository)
    {
        $this->taskRepository = $taskRepository;
        $this->userRepository = $userRepository;
    }

    /**
     * 全タスクを取得
     *
     * @return Collection
     */
    public function getAllTasks(): Collection
    {
        return $this->taskRepository->getAll();
    }

    /**
     * IDでタスクを取得
     *
     * @param int $id
     * @return Task|null
     */
    public function getTaskById(int $id): ?Task
    {
        return $this->taskRepository->findById($id);
    }

    /**
     * タスクを作成
     *
     * @param array $data
     * @return Task
     * @throws \Exception
     */
    public function createTask(array $data): Task
    {
        // ユーザーIDの存在チェック
        if (!$this->userRepository->findById($data['user_id'])) {
            throw new \Exception('指定されたユーザーが存在しません。');
        }

        $task = $this->taskRepository->create($data);
        return $this->taskRepository->findById($task->id);
    }

    /**
     * タスクを更新
     *
     * @param int $id
     * @param array $data
     * @return Task|null
     * @throws \Exception
     */
    public function updateTask(int $id, array $data): ?Task
    {
        $task = $this->taskRepository->findById($id);
        if (!$task) {
            return null;
        }

        // ユーザーIDの存在チェック(更新時)
        if (isset($data['user_id']) && !$this->userRepository->findById($data['user_id'])) {
            throw new \Exception('指定されたユーザーが存在しません。');
        }

        return $this->taskRepository->update($id, $data);
    }

    /**
     * タスクを削除
     *
     * @param int $id
     * @return bool
     */
    public function deleteTask(int $id): bool
    {
        return $this->taskRepository->delete($id);
    }

    /**
     * ユーザーIDでタスクを取得
     *
     * @param int $userId
     * @return Collection
     */
    public function getTasksByUserId(int $userId): Collection
    {
        return $this->taskRepository->findByUserId($userId);
    }
}

CreateUsersTable.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

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

CreateTasksTable.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateTasksTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('user_id');
            $table->text('title');
            $table->text('detail');
            $table->timestamps();

            // 外部キー制約
            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('tasks');
    }
}
株式会社アクトビ

Discussion