🙄

Claude CodeのOutput Styleを色々使ってみた

に公開

はじめに

こんにちは。アクトビの村崎です。
弊社ではAIエージェントにCursorを採用しているのですが、最近Claude Codeの導入を検討していてその中でOutput Styleの変更機能が目に留まったので、実際に触ってみた感想をここで共有したいと思います。

Output Styleとは?

Output Styles は、Claudeの応答方法(フォーマット・トーン・構造)を変更する機能で、選択すると常に有効になります。
応答方法の種類は以下の3種類から選ぶことができます。

  • Default
  • Explanatory
  • Learning

Default

ソフトウェアエンジニアリングタスクを効率よく完了するために設計された既存のシステムプロンプトがそのまま使われます。
特に何も設定しなかったら自動で選択されているモードになります。

Explanatory

タスク実行の合間に「Insights」セクションを挿入し、実装の選択やコードパターンを教育的に解説してくれます。
新しいコードベースを理解したいときに有効です。

Learning

学びながら進むコラボモード。Insightsの共有に加え、TODO(human) マーカーをコードに挿入して、ユーザー自身にも小さなコーディングを促します。
ジュニアエンジニアのオンボーディングやペアプログラミングに特に有効です。

サンプルのプロジェクトについて

タスク管理ツールに利用するAPIを実装するLaravelプロジェクトで実践したいと思います。
機能としてはタスクの登録、更新、完了の3つを用意したいと思います。


技術スタック

  • PHP: 8.0+
  • Framework: Laravel 8.x
  • DB: MySQL 8.0
  • 認証: Laravel Sanctum

ディレクトリ構成

app/
├── Http/
│   ├── Controllers/
│   │   └── Api/
│   │       ├── TaskController.php
│   │       ├── UserController.php
│   │       └── AuthController.php
│   ├── Requests/
│   │   ├── Task/
│   │   │   ├── StoreTaskRequest.php
│   │   │   ├── UpdateTaskRequest.php
│   │   │   └── CompleteTaskRequest.php
│   │   ├── User/
│   │   │   ├── StoreUserRequest.php
│   │   │   └── UpdateUserRequest.php
│   │   └── Auth/
│   │       └── LoginRequest.php
│   └── Resources/
│       ├── Task/
│       │   ├── TaskResource.php
│       │   └── TaskCollection.php
│       └── User/
│           └── UserResource.php
├── Services/
│   ├── TaskService.php
│   ├── UserService.php
│   └── AuthService.php
├── Usecases/
│   └── Task/
│       └── CompleteTaskUsecase.php
├── Repositories/
│   ├── Task/
│   │   ├── TaskRepositoryInterface.php
│   │   └── TaskRepository.php
│   └── User/
│       ├── UserRepositoryInterface.php
│       └── UserRepository.php
└── Models/
    ├── Task.php
    └── User.php

テーブル定義

users テーブル

カラム名 NULL デフォルト 備考
id BIGINT UNSIGNED PK / AUTO INCREMENT
name VARCHAR(255) ユーザー名
email VARCHAR(255) UNIQUE
email_verified_at TIMESTAMP NULL メール認証日時
password VARCHAR(255) ハッシュ済みパスワード
remember_token VARCHAR(100) NULL ログイン保持トークン
created_at TIMESTAMP NULL レコード作成日時
updated_at TIMESTAMP NULL レコード更新日時
deleted_at TIMESTAMP NULL 論理削除日時

tasks テーブル

カラム名 NULL デフォルト 備考
id BIGINT UNSIGNED PK / AUTO INCREMENT
user_id BIGINT UNSIGNED FK → users.id(CASCADE DELETE)
title VARCHAR(255) タスク名
description TEXT NULL タスク詳細
status ENUM todo todo / in_progress / done
priority ENUM medium low / medium / high
due_date DATE NULL 期限日
completed_at TIMESTAMP NULL 完了日時
created_at TIMESTAMP NULL レコード作成日時
updated_at TIMESTAMP NULL レコード更新日時
deleted_at TIMESTAMP NULL 論理削除日時

インデックス

インデックス名 カラム 種別 用途
PRIMARY id PRIMARY KEY
users_email_unique email UNIQUE メール重複防止
tasks_user_id_status_index user_id, status INDEX ステータス絞り込み
tasks_user_id_priority_index user_id, priority INDEX 優先度絞り込み
tasks_user_id_due_date_index user_id, due_date INDEX 期限日ソート・絞り込み

リレーション

テーブル カラム 参照先 ON DELETE
tasks user_id users.id CASCADE

Output Styleの変更について

切り替えをするときは/configコマンドで指定が可能。

Auto-compact                              true
    Show tips                                 true
    Reduce motion                             false
    Thinking mode                             true
    Rewind code (checkpoints)                 true
    Verbose output                            false
    Terminal progress bar                     true
    Show turn duration                        true
    Default permission mode                   Default
    Respect .gitignore in file picker         true
    Always copy full response (skip /copy     false
  picker)
    Auto-update channel                       latest
    Theme                                     Light mode
    Notifications                             Auto
  ❯ Output style                              default
    Language                                  Default (English)
    Editor mode                               normal
    Show PR status footer                     true
    Model                                     Default (recommended)
    Auto-connect to IDE (external terminal)   false

Output Styleを選択してスペースキーを押すことでモード選択が可能。

This changes how Claude Code communicates with you

  ❯ 1. Default ✔    Claude completes coding tasks efficiently and provides concise responses
    2. Explanatory  Claude explains its implementation choices and codebase patterns
    3. Learning     Claude pauses and asks you to write small pieces of code for hands-on practice

比較検証

マイグレーションファイルの作成

テーブル定義は予めCLAUDE.mdに記載しておいたので、以下のプロンプトでtasksテーブルのマイグレーションファイルを作成してみます。

{プロジェクト名}/database/migrations/CLAUDE.mdに記載したtasksテーブルのマイグレーションファイルを作成して。

Defaultモードで実行した結果

Write(database/migrations/2026_03_23_000000_create_tasks_table.php)
  ⎿  Wrote 33 lines to database/migrations/2026_03_23_000000_create_tasks_table.php
       1 <?php
       2
       3 use Illuminate\Database\Migrations\Migration;
       4 use Illuminate\Database\Schema\Blueprint;
       5 use Illuminate\Support\Facades\Schema;
       6
       7 class CreateTasksTable extends Migration
       8 {
       9     public function up(): void
      10     {
     … +23 lines (ctrl+o to expand)

⏺ 2026_03_23_000000_create_tasks_table.php を作成しました。

  CLAUDE.md の定義に基づき、以下を実装しています:

  - user_id — users.id への外部キー(CASCADE DELETE)
  - status — ENUM (todo / in_progress / done)、デフォルト todo
  - priority — ENUM (low / medium / high)、デフォルト medium
  - completed_at — タスク完了日時(nullable)
  - softDeletes() — 論理削除用 deleted_at
  - 複合インデックス3件(user_id + status / priority / due_date)

Explanatoryモードで実行した結果

⏺ CLAUDE.md のテーブル定義を確認しました。tasks テーブルのマイグレーションファイルを作成します。

  ★ Insight ─────────────────────────────────────
  - ENUM 型はLaravelの ->default() と組み合わせることでDB側にデフォルト値を持たせられる
  - user_id への外部キーに cascadeOnDelete() を指定することで、ユーザー削除時に関連タスクも自動削除される
  - 複合インデックス(user_id + status 等)は、WHERE句で両カラムを使うクエリのパフォーマンスを大幅に改善する
  ─────────────────────────────────────────────────

⏺ Write(database/migrations/2026_03_23_000000_create_tasks_table.php)
  ⎿  Wrote 45 lines to database/migrations/2026_03_23_000000_create_tasks_table.php
       1 <?php
       2
       3 use Illuminate\Database\Migrations\Migration;
       4 use Illuminate\Database\Schema\Blueprint;
       5 use Illuminate\Support\Facades\Schema;
       6
       7 class CreateTasksTable extends Migration
       8 {
       9     /**
      10      * Run the migrations.
     … +35 lines (ctrl+o to expand)

⏺ database/migrations/2026_03_23_000000_create_tasks_table.php を作成しました。

  CLAUDE.md の定義に基づき、以下の内容を実装しています:

  ┌────────────────────────────────────────────────────────┬──────────────────────────────────────────────────────────┐
  │                        実装内容                        │                           詳細                           │
  ├────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────┤
  │ foreignId('user_id')->constrained()->cascadeOnDelete() │ users.id への FK + CASCADE DELETE                        │
  ├────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────┤
  │ enum('status', [...]) / enum('priority', [...])        │ デフォルト値 todo / medium 付き                          │
  ├────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────┤
  │ softDeletes()                                          │ deleted_at カラムによる論理削除                          │
  ├────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────┤
  │ 複合インデックス × 3                                   │ user_id + status, user_id + priority, user_id + due_date │
  └────────────────────────────────────────────────────────┴──────────────────────────────────────────────────────────┘

  ★ Insight ─────────────────────────────────────
  - foreignId('user_id')->constrained() は $table->unsignedBigInteger('user_id') + $table->foreign('user_id')->references('id')->on('users') の省略形。テーブル名を自動推定してくれるため記述量を削減できる
  - softDeletes() を使うと Model 側で use SoftDeletes トレイトを追加するだけで、delete() が物理削除でなく deleted_at セットに変わる。CLAUDE.md の Task モデルにも追加が必要
  - マイグレーションファイル名のタイムスタンプ部分は実行順序を決定する。users テーブルが先に存在していないと FK 制約でエラーになるため、users テーブルのマイグレーションより後のタイムスタンプにしている
  ─────────────────────────────────────────────────

Defaultモードには存在しないInsightという項目が追加されて、そこに注意事項やメソッドの説明が記載されていますね。
Cascadeとか複合インデックスとか、経験の浅いエンジニアにとっては何?ってなりそうな項目についてもInsightで解説してくれてますね!
今回の改修による次回アクションまで提示してくれるので、改修のサポートまで実行してくれています!

Learningモードで実行した結果

⏺ ★ Insight ─────────────────────────────────────
  - Laravelのマイグレーションでは enum() よりも string() + ->default() を使うケースもあるが、ENUMはDBレベルで値を制約できるため、ここではENUM型が適切
  - softDeletes() は deleted_at カラムを追加するヘルパーで、Eloquentの SoftDeletes トレイトと対応する
  - 複合インデックスは ->index(['user_id', 'status']) のように配列で指定でき、クエリの絞り込み・ソートを高速化する
  ─────────────────────────────────────────────────

⏺ Write(database/migrations/2026_03_23_000000_create_tasks_table.php)
  ⎿  Wrote 33 lines to database/migrations/2026_03_23_000000_create_tasks_table.php
       1 <?php
       2
       3 use Illuminate\Database\Migrations\Migration;
       4 use Illuminate\Database\Schema\Blueprint;
       5 use Illuminate\Support\Facades\Schema;
       6
       7 class CreateTasksTable extends Migration
       8 {
       9     public function up(): void
      10     {
     … +23 lines (ctrl+o to expand)

⏺ 2026_03_23_000000_create_tasks_table.php を作成しました。

  CLAUDE.mdの定義を元に以下を実装しています:

  ┌────────────────────────────────────┬────────────────────────────────────────────────────────┐
  │                内容                │                          実装                          │
  ├────────────────────────────────────┼────────────────────────────────────────────────────────┤
  │ FK + CASCADE DELETE                │ foreignId('user_id')->constrained()->cascadeOnDelete() │
  ├────────────────────────────────────┼────────────────────────────────────────────────────────┤
  │ status ENUM (デフォルト: todo)     │ enum(...)->default('todo')                             │
  ├────────────────────────────────────┼────────────────────────────────────────────────────────┤
  │ priority ENUM (デフォルト: medium) │ enum(...)->default('medium')                           │
  ├────────────────────────────────────┼────────────────────────────────────────────────────────┤
  │ 論理削除                           │ softDeletes() → deleted_at                             │
  ├────────────────────────────────────┼────────────────────────────────────────────────────────┤
  │ 3つの複合インデックス              │ index(['user_id', 'status']) など                      │
  └────────────────────────────────────┴────────────────────────────────────────────────────────┘

  ★ Insight ─────────────────────────────────────
  - foreignId('user_id')->constrained() は users.id への外部キー制約をLaravelの命名規則で自動解決する省略記法。->references('id')->on('users') と等価
  - cascadeOnDelete() によりユーザー削除時にそのタスクも自動削除される。SoftDeletes と組み合わせる場合、usersテーブルの論理削除では発火しない点に注意
  ─────────────────────────────────────────────────

うーん。Explanatoryモードとの違いが特にわからないですね。。。笑

タスクの作成APIの実装

次はタスクの新規登録のAPIを実装することで比較をしてみます。
プロンプトは全て以下の通りに投げました。

タスクの新規登録用のAPIを実装して

Defaultモードで実行した結果

⏺ Explore(Explore existing codebase structure)
  ⎿  Done (30 tool uses · 42.0k tokens · 34s)

⏺ 既存ファイルの内容を確認してから実装します。
  ⎿  Loaded database/migrations/CLAUDE.md


⏺ Write(app/Models/Task.php)
  ⎿  Wrote 33 lines to app/Models/Task.php
       1 <?php
       2
       3 namespace App\Models;
       4
       5 use Illuminate\Database\Eloquent\Factories\HasFactory;
       6 use Illuminate\Database\Eloquent\Model;
       7 use Illuminate\Database\Eloquent\Relations\BelongsTo;
       8 use Illuminate\Database\Eloquent\SoftDeletes;
       9
      10 class Task extends Model
     … +23 lines (ctrl+o to expand)

⏺ Write(app/Repositories/Task/TaskRepositoryInterface.php)
  ⎿  Wrote 11 lines to app/Repositories/Task/TaskRepositoryInterface.php
       1 <?php
       2
       3 namespace App\Repositories\Task;
       4
       5 use App\Models\Task;
       6
       7 interface TaskRepositoryInterface
       8 {
       9     public function create(array $data): Task;
      10     public function existsByTitleForUser(string $title, int $userId): bool;
     … +1 line (ctrl+o to expand)

⏺ Write(app/Repositories/Task/TaskRepository.php)
  ⎿  Wrote 18 lines to app/Repositories/Task/TaskRepository.php
       1 <?php
       2
       3 namespace App\Repositories\Task;
       4
       5 use App\Models\Task;
       6
       7 class TaskRepository implements TaskRepositoryInterface
       8 {
       9     public function create(array $data): Task
      10     {
     … +8 lines (ctrl+o to expand)

⏺ Write(app/Services/TaskService.php)
  ⎿  Wrote 23 lines to app/Services/TaskService.php
       1 <?php
       2
       3 namespace App\Services;
       4
       5 use App\Exceptions\DuplicateTaskException;
       6 use App\Models\Task;
       7 use App\Repositories\Task\TaskRepositoryInterface;
       8
       9 class TaskService
      10 {
     … +13 lines (ctrl+o to expand)

⏺ Write(app/Http/Requests/Task/StoreTaskRequest.php)
  ⎿  Wrote 25 lines to app/Http/Requests/Task/StoreTaskRequest.php
       1 <?php
       2
       3 namespace App\Http\Requests\Task;
       4
       5 use Illuminate\Foundation\Http\FormRequest;
       6 use Illuminate\Validation\Rule;
       7
       8 class StoreTaskRequest extends FormRequest
       9 {
      10     public function authorize(): bool
     … +15 lines (ctrl+o to expand)

⏺ Write(app/Http/Resources/Task/TaskResource.php)
  ⎿  Wrote 22 lines to app/Http/Resources/Task/TaskResource.php
       1 <?php
       2
       3 namespace App\Http\Resources\Task;
       4
       5 use Illuminate\Http\Resources\Json\JsonResource;
       6
       7 class TaskResource extends JsonResource
       8 {
       9     public function toArray($request): array
      10     {
     … +12 lines (ctrl+o to expand)

⏺ Write(app/Http/Resources/Task/TaskCollection.php)
  ⎿  Wrote 20 lines to app/Http/Resources/Task/TaskCollection.php
      1 <?php
      2
      3 namespace App\Http\Resources\Task;
      4
      5 use Illuminate\Http\Resources\Json\ResourceCollection;
      6
      7 class TaskCollection extends ResourceCollection
      8 {
      9     public $collects = TaskResource::class;
     … +10 lines (ctrl+o to expand)

⏺ Write(app/Http/Controllers/Api/TaskController.php)
  ⎿  Wrote 24 lines to app/Http/Controllers/Api/TaskController.php
      1 <?php
      2
      3 namespace App\Http\Controllers\Api;
      4
      5 use App\Http\Controllers\Controller;
      6 use App\Http\Requests\Task\StoreTaskRequest;
      7 use App\Http\Resources\Task\TaskResource;
      8 use App\Services\TaskService;
      9 use Illuminate\Http\JsonResponse;
     … +14 lines (ctrl+o to expand)

⏺ Write(app/Exceptions/DuplicateTaskException.php)
  ⎿  Wrote 7 lines to app/Exceptions/DuplicateTaskException.php
      1 <?php
      2
      3 namespace App\Exceptions;
      4
      5 use Exception;
      6
      7 class DuplicateTaskException extends Exception {}

⏺ 次に Handler.php を確認して例外ハンドリングを追加します。


⏺ Update(app/Exceptions/Handler.php)
  ⎿  Added 1 line
      3  namespace App\Exceptions;
      4
      5  use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
      6 +use Illuminate\Http\JsonResponse;
      7  use Throwable;
      8
      9  class Handler extends ExceptionHandler

⏺ Update(app/Exceptions/Handler.php)
  ⎿  Added 4 lines
      38          $this->reportable(function (Throwable $e) {
      39              //
      40          });
      41 +
      42 +        $this->renderable(function (DuplicateTaskException $e) {
      43 +            return new JsonResponse(['message' => $e->getMessage()], 409);
      44 +        });
      45      }
      46  }

⏺ Update(app/Providers/AppServiceProvider.php)
  ⎿  Added 6 lines, removed 1 line
      2
      3  namespace App\Providers;
      4
      5 +use App\Repositories\Task\TaskRepository;
      6 +use App\Repositories\Task\TaskRepositoryInterface;
      7  use Illuminate\Support\ServiceProvider;
      8
      9  class AppServiceProvider extends ServiceProvider
      15       */
      16      public function register()
      17      {
      16 -        //
      18 +        $this->app->bind(
      19 +            TaskRepositoryInterface::class,
      20 +            TaskRepository::class,
      21 +        );
      22      }
      23
      24      /**

⏺ Update(routes/api.php)
  ⎿  Added 5 lines
      1  <?php
      2
      3 +use App\Http\Controllers\Api\TaskController;
      4  use Illuminate\Http\Request;
      5  use Illuminate\Support\Facades\Route;
      6
      18  Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
      19      return $request->user();
      20  });
      21 +
      22 +Route::middleware('auth:sanctum')->group(function () {
      23 +    Route::post('/tasks', [TaskController::class, 'store']);
      24 +});

⏺ 実装完了です。作成・更新したファイルの一覧:

  ┌───────────────────────────────────────────────────┬──────────────────────────────────┐
  │                     ファイル                      │               内容               │
  ├───────────────────────────────────────────────────┼──────────────────────────────────┤
  │ app/Models/Task.php                               │ Task モデル(SoftDeletes 含む)  │
  ├───────────────────────────────────────────────────┼──────────────────────────────────┤
  │ app/Repositories/Task/TaskRepositoryInterface.php │ Repository インターフェース      │
  ├───────────────────────────────────────────────────┼──────────────────────────────────┤
  │ app/Repositories/Task/TaskRepository.php          │ Repository 実装                  │
  ├───────────────────────────────────────────────────┼──────────────────────────────────┤
  │ app/Services/TaskService.php                      │ 重複チェック付きビジネスロジック │
  ├───────────────────────────────────────────────────┼──────────────────────────────────┤
  │ app/Http/Requests/Task/StoreTaskRequest.php       │ バリデーション                   │
  ├───────────────────────────────────────────────────┼──────────────────────────────────┤
  │ app/Http/Resources/Task/TaskResource.php          │ レスポンス整形                   │
  ├───────────────────────────────────────────────────┼──────────────────────────────────┤
  │ app/Http/Resources/Task/TaskCollection.php        │ 一覧用コレクション               │
  ├───────────────────────────────────────────────────┼──────────────────────────────────┤
  │ app/Http/Controllers/Api/TaskController.php       │ POST /api/tasks エンドポイント   │
  ├───────────────────────────────────────────────────┼──────────────────────────────────┤
  │ app/Exceptions/DuplicateTaskException.php         │ 重複タスク例外                   │
  ├───────────────────────────────────────────────────┼──────────────────────────────────┤
  │ app/Exceptions/Handler.php                        │ 例外 → HTTP 409 変換を追加       │
  ├───────────────────────────────────────────────────┼──────────────────────────────────┤
  │ app/Providers/AppServiceProvider.php              │ Repository バインド追加          │
  ├───────────────────────────────────────────────────┼──────────────────────────────────┤
  │ routes/api.php                                    │ POST /api/tasks ルート追加       │
  └───────────────────────────────────────────────────┴──────────────────────────────────┘

  エンドポイント仕様

  POST /api/tasks
  Authorization: Bearer {sanctum_token}
  Content-Type: application/json

  {
    "title": "タスク名",        // required, max:255
    "description": "詳細",     // nullable
    "status": "todo",          // required: todo|in_progress|done
    "priority": "medium",      // required: low|medium|high
    "due_date": "2026-04-01"   // nullable, after_or_equal:today
  }

  レスポンス
  - 201 — 作成成功(TaskResource 形式)
  - 409 — 同一ユーザーで同名タスクが既に存在
  - 422 — バリデーションエラー
  - 401 — 未認証

Explanatoryモードで実行した結果

⏺ まず既存のコードベースを確認します。
  ⎿  Loaded database/migrations/CLAUDE.md


⏺ スキーマと構成を把握しました。以下のファイルをすべて実装します。

  ★ Insight ─────────────────────────────────────
  - マイグレーションは既に存在 (tasksテーブル・ソフトデリート・複合インデックス付き)。実装すべき層は Model → Repository → Service → FormRequest → Resource → Controller の順。
  - CLAUDE.mdの原則に従い、Repositoryインターフェースを介した依存性逆転 (DIP) を徹底する。これにより、テスト時にモックへの差し替えが容易になる。
  - DuplicateTaskException のような domain-specific例外を定義し、Handler.phpで一元的にHTTPレスポンスへ変換することで、Controllerをビジネスロジックから完全に切り離す。
  ─────────────────────────────────────────────────

⏺ Write(app/Models/Task.php)
  ⎿  Wrote 33 lines to app/Models/Task.php
       1 <?php
       2
       3 namespace App\Models;
       4
       5 use Illuminate\Database\Eloquent\Factories\HasFactory;
       6 use Illuminate\Database\Eloquent\Model;
       7 use Illuminate\Database\Eloquent\Relations\BelongsTo;
       8 use Illuminate\Database\Eloquent\SoftDeletes;
       9
      10 class Task extends Model
     … +23 lines (ctrl+o to expand)

⏺ Write(app/Repositories/Task/TaskRepositoryInterface.php)
  ⎿  Wrote 11 lines to app/Repositories/Task/TaskRepositoryInterface.php
       1 <?php
       2
       3 namespace App\Repositories\Task;
       4
       5 use App\Models\Task;
       6
       7 interface TaskRepositoryInterface
       8 {
       9     public function create(array $data): Task;
      10     public function existsByTitleForUser(string $title, int $userId): bool;
     … +1 line (ctrl+o to expand)

⏺ Write(app/Repositories/Task/TaskRepository.php)
  ⎿  Wrote 18 lines to app/Repositories/Task/TaskRepository.php
       1 <?php
       2
       3 namespace App\Repositories\Task;
       4
       5 use App\Models\Task;
       6
       7 class TaskRepository implements TaskRepositoryInterface
       8 {
       9     public function create(array $data): Task
      10     {
     … +8 lines (ctrl+o to expand)

⏺ Write(app/Exceptions/DuplicateTaskException.php)
  ⎿  Wrote 7 lines to app/Exceptions/DuplicateTaskException.php
      1 <?php
      2
      3 namespace App\Exceptions;
      4
      5 use RuntimeException;
      6
      7 class DuplicateTaskException extends RuntimeException {}

⏺ Write(app/Services/TaskService.php)
  ⎿  Wrote 23 lines to app/Services/TaskService.php
       1 <?php
       2
       3 namespace App\Services;
       4
       5 use App\Exceptions\DuplicateTaskException;
       6 use App\Models\Task;
       7 use App\Repositories\Task\TaskRepositoryInterface;
       8
       9 class TaskService
      10 {
     … +13 lines (ctrl+o to expand)

⏺ Write(app/Http/Requests/Task/StoreTaskRequest.php)
  ⎿  Wrote 32 lines to app/Http/Requests/Task/StoreTaskRequest.php
       1 <?php
       2
       3 namespace App\Http\Requests\Task;
       4
       5 use Illuminate\Foundation\Http\FormRequest;
       6 use Illuminate\Validation\Rule;
       7
       8 class StoreTaskRequest extends FormRequest
       9 {
      10     public function authorize(): bool
     … +22 lines (ctrl+o to expand)

⏺ Write(app/Http/Resources/Task/TaskResource.php)
  ⎿  Wrote 22 lines to app/Http/Resources/Task/TaskResource.php
       1 <?php
       2
       3 namespace App\Http\Resources\Task;
       4
       5 use Illuminate\Http\Resources\Json\JsonResource;
       6
       7 class TaskResource extends JsonResource
       8 {
       9     public function toArray($request): array
      10     {
     … +12 lines (ctrl+o to expand)

⏺ Write(app/Http/Controllers/Api/TaskController.php)
  ⎿  Wrote 23 lines to app/Http/Controllers/Api/TaskController.php
      1 <?php
      2
      3 namespace App\Http\Controllers\Api;
      4
      5 use App\Http\Controllers\Controller;
      6 use App\Http\Requests\Task\StoreTaskRequest;
      7 use App\Http\Resources\Task\TaskResource;
      8 use App\Services\TaskService;
      9 use Illuminate\Http\JsonResponse;
     … +13 lines (ctrl+o to expand)

⏺ 次に、AppServiceProviderでバインドを設定し、Handler.phpで例外をHTTPレスポンスに変換します。

⏺ Update(app/Providers/AppServiceProvider.php)
  ⎿  Added 6 lines, removed 1 line
      2
      3  namespace App\Providers;
      4
      5 +use App\Repositories\Task\TaskRepository;
      6 +use App\Repositories\Task\TaskRepositoryInterface;
      7  use Illuminate\Support\ServiceProvider;
      8
      9  class AppServiceProvider extends ServiceProvider
      15       */
      16      public function register()
      17      {
      16 -        //
      18 +        $this->app->bind(
      19 +            TaskRepositoryInterface::class,
      20 +            TaskRepository::class,
      21 +        );
      22      }
      23
      24      /**

⏺ Update(app/Exceptions/Handler.php)
  ⎿  Added 2 lines
       2
       3  namespace App\Exceptions;
       4
       5 +use App\Exceptions\DuplicateTaskException;
       6  use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
       7 +use Illuminate\Http\JsonResponse;
       8  use Throwable;
       9
      10  class Handler extends ExceptionHandler

⏺ Update(app/Exceptions/Handler.php)
  ⎿  Added 4 lines
      39          $this->reportable(function (Throwable $e) {
      40              //
      41          });
      42 +
      43 +        $this->renderable(function (DuplicateTaskException $e) {
      44 +            return new JsonResponse(['message' => $e->getMessage()], 409);
      45 +        });
      46      }
      47  }

⏺ 最後にルートを追加します。

⏺ Update(routes/api.php)
  ⎿  Added 1 line
      1  <?php
      2
      3 +use App\Http\Controllers\Api\TaskController;
      4  use Illuminate\Http\Request;
      5  use Illuminate\Support\Facades\Route;
      6

⏺ Update(routes/api.php)
  ⎿  Added 4 lines
      18  Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
      19      return $request->user();
      20  });
      21 +
      22 +Route::middleware('auth:sanctum')->group(function () {
      23 +    Route::post('/tasks', [TaskController::class, 'store']);
      24 +});

⏺ 実装完了です。作成したファイルと変更を整理します。

  ---
  実装内容

  ┌───────────────────────────────────────────────────┬──────────────────────────────────────┐
  │                     ファイル                      │                 役割                 │
  ├───────────────────────────────────────────────────┼──────────────────────────────────────┤
  │ app/Models/Task.php                               │ Taskモデル (SoftDeletes, casts設定)  │
  ├───────────────────────────────────────────────────┼──────────────────────────────────────┤
  │ app/Repositories/Task/TaskRepositoryInterface.php │ リポジトリのインターフェース         │
  ├───────────────────────────────────────────────────┼──────────────────────────────────────┤
  │ app/Repositories/Task/TaskRepository.php          │ Eloquentによるデータアクセス実装     │
  ├───────────────────────────────────────────────────┼──────────────────────────────────────┤
  │ app/Exceptions/DuplicateTaskException.php         │ 重複タスクのドメイン例外             │
  ├───────────────────────────────────────────────────┼──────────────────────────────────────┤
  │ app/Services/TaskService.php                      │ 重複チェック + 作成ロジック          │
  ├───────────────────────────────────────────────────┼──────────────────────────────────────┤
  │ app/Http/Requests/Task/StoreTaskRequest.php       │ バリデーション + user_id付与         │
  ├───────────────────────────────────────────────────┼──────────────────────────────────────┤
  │ app/Http/Resources/Task/TaskResource.php          │ レスポンスJSONの整形                 │
  ├───────────────────────────────────────────────────┼──────────────────────────────────────┤
  │ app/Http/Controllers/Api/TaskController.php       │ ルーティング・レスポンスのみ担当     │
  ├───────────────────────────────────────────────────┼──────────────────────────────────────┤
  │ app/Providers/AppServiceProvider.php              │ Interface → Repositoryのバインド追加 │
  ├───────────────────────────────────────────────────┼──────────────────────────────────────┤
  │ app/Exceptions/Handler.php                        │ DuplicateTaskException → 409に変換   │
  ├───────────────────────────────────────────────────┼──────────────────────────────────────┤
  │ routes/api.php                                    │ POST /api/tasks ルート追加           │
  └───────────────────────────────────────────────────┴──────────────────────────────────────┘

  ---
  APIの動作

  リクエスト例
  POST /api/tasks
  Authorization: Bearer {sanctum_token}
  Content-Type: application/json

  {
    "title": "要件定義書を作成",
    "description": "プロジェクトの要件を整理する",
    "status": "todo",
    "priority": "high",
    "due_date": "2026-04-01"
  }

  レスポンス例 (201)
  {
    "id": 1,
    "title": "要件定義書を作成",
    "description": "プロジェクトの要件を整理する",
    "status": "todo",
    "priority": "high",
    "due_date": "2026-04-01",
    "created_at": "2026-03-26T00:00:00+00:00",
    "updated_at": "2026-03-26T00:00:00+00:00"
  }

  エラーレスポンス (409) — 同一ユーザーで同名タスクが既存の場合
  { "message": "同名のタスクが既に存在します。" }

  ---
  ★ Insight ─────────────────────────────────────
  - StoreTaskRequest::validated() をオーバーライドして user_id を付与している。これによりControllerは $request->user()->id を意識する必要がなく、認証済みユーザーとの紐付けがリクエスト層で完結する。
  - Handler::renderable() を使うと例外クラスとHTTPステータスを1対1で宣言的に対応付けられる。今後ドメイン例外が増えても、Controllerに一切手を加えずにここだけ追記すればよい。
  - Repositoryインターフェースのメソッドは今回の store 実装に必要な2つのみに絞っている。他のエンドポイント (一覧・更新・削除) 実装時に順次追加するのが最小設計の原則に沿っている。
  ─────────────────────────────────────────────────

今回もInsightで補足してくれていますね。
CLAUDE.mdファイルを事前に定義しているため、そこで記載されている内容に触れて処理のポイントをまとめてくれていますね。
(※今回のは内容が難しくて中級者のエンジニアじゃないと理解が厳しいかも。。。)
心なしかDefaultモードよりもAPIのレスポンス例が詳細に書かれていて読みやすいかも。

Learningモードで実行した結果

⏺ まず現在のコードベースを確認します。


⏺ スキーマを確認しました。CLAUDE.mdのアーキテクチャに沿って実装します。

  ★ Insight ─────────────────────────────────────
  Laravelの層構造では、各層が単一の責務を持ちます。FormRequestはバリデーション専用、Serviceはビジネスルール専用、Repositoryはデータアクセス専用に分離することで、テストが容易になり変更の影響範囲を限定できます。
  ─────────────────────────────────────────────────

⏺ Write(app/Models/Task.php)
  ⎿  Wrote 33 lines to app/Models/Task.php
       1 <?php
       2
       3 namespace App\Models;
       4
       5 use Illuminate\Database\Eloquent\Factories\HasFactory;
       6 use Illuminate\Database\Eloquent\Model;
       7 use Illuminate\Database\Eloquent\Relations\BelongsTo;
       8 use Illuminate\Database\Eloquent\SoftDeletes;
       9
      10 class Task extends Model
     … +23 lines (ctrl+o to expand)

⏺ Bash(mkdir -p {プロジェクト名}/app/Repositories/Task)
  ⎿  Done

⏺ Write(app/Repositories/Task/TaskRepositoryInterface.php)
  ⎿  Wrote 11 lines to app/Repositories/Task/TaskRepositoryInterface.php
       1 <?php
       2
       3 namespace App\Repositories\Task;
       4
       5 use App\Models\Task;
       6
       7 interface TaskRepositoryInterface
       8 {
       9     public function create(array $data): Task;
      10     public function existsByTitleForUser(string $title, int $userId): bool;
     … +1 line (ctrl+o to expand)

⏺ Write(app/Repositories/Task/TaskRepository.php)
  ⎿  Wrote 18 lines to app/Repositories/Task/TaskRepository.php
       1 <?php
       2
       3 namespace App\Repositories\Task;
       4
       5 use App\Models\Task;
       6
       7 class TaskRepository implements TaskRepositoryInterface
       8 {
       9     public function create(array $data): Task
      10     {
     … +8 lines (ctrl+o to expand)

⏺ Bash(mkdir -p {プロジェクト名}/app/Http/Requests/Task)
  ⎿  Done

⏺ Write(app/Http/Requests/Task/StoreTaskRequest.php)
  ⎿  Wrote 25 lines to app/Http/Requests/Task/StoreTaskRequest.php
       1 <?php
       2
       3 namespace App\Http\Requests\Task;
       4
       5 use Illuminate\Foundation\Http\FormRequest;
       6 use Illuminate\Validation\Rule;
       7
       8 class StoreTaskRequest extends FormRequest
       9 {
      10     public function authorize(): bool
     … +15 lines (ctrl+o to expand)

⏺ Bash(mkdir -p {プロジェクト名}/app/Http/Resources/Task)
  ⎿  Done

⏺ Write(app/Http/Resources/Task/TaskResource.php)
  ⎿  Wrote 22 lines to app/Http/Resources/Task/TaskResource.php
       1 <?php
       2
       3 namespace App\Http\Resources\Task;
       4
       5 use Illuminate\Http\Resources\Json\JsonResource;
       6
       7 class TaskResource extends JsonResource
       8 {
       9     public function toArray($request): array
      10     {
     … +12 lines (ctrl+o to expand)

⏺ 次に、TaskService の骨格を作りますが、ビジネスロジックの核心部分を実装してもらいます。

⏺ Bash(mkdir -p {プロジェクト名}/app/Services {プロジェクト名}…)
  ⎿  Done

⏺ Write(app/Exceptions/DuplicateTaskException.php)
  ⎿  Wrote 7 lines to app/Exceptions/DuplicateTaskException.php
      1 <?php
      2
      3 namespace App\Exceptions;
      4
      5 use RuntimeException;
      6
      7 class DuplicateTaskException extends RuntimeException {}

⏺ Write(app/Services/TaskService.php)
  ⎿  Wrote 21 lines to app/Services/TaskService.php
       1 <?php
       2
       3 namespace App\Services;
       4
       5 use App\Exceptions\DuplicateTaskException;
       6 use App\Models\Task;
       7 use App\Repositories\Task\TaskRepositoryInterface;
       8
       9 class TaskService
      10 {
     … +11 lines (ctrl+o to expand)

⏺ ---
  ● Learn by Doing

  Context: Task Model・Repository・FormRequest・ResourceのすべてのインフラはOKです。Service層が唯一のビジネスルールの入口で、「同名タスクの重複禁止」というドメインルールをここで守ります。

  Your Task: app/Services/TaskService.php の create() メソッドを実装してください。TODO(human) のコメントを見てください。

  Guidance:
  - $data['user_id'] と $data['title'] を使って $this->taskRepository->existsByTitleForUser() で重複チェックを行う
  - 重複していれば DuplicateTaskException をスローする(メッセージは自由)
  - 問題なければ $this->taskRepository->create($data) でタスクを作成して返す
  - 重複チェックをServiceに置く理由を意識してみてください(FormRequestではなくServiceにある理由は何でしょう?)

✻ Worked for 1m 33s

  7 tasks (4 done, 1 in progress, 2 open)
  ✔ Task Modelの作成
  ✔ TaskRepositoryInterface / TaskRepository の作成
  ✔ StoreTaskRequest の作成
  ✔ TaskResource の作成
  ◼ TaskService の作成(ビジネスロジックは人間に依頼)
  ◻ TaskController の作成
  ◻ AppServiceProvider と routes/api.php の更新

お!Your Taskと書かれていて、「app/Services/TaskService.php の create() メソッドを実装してください。TODO(human) のコメントを見てください。」の指示が入っていますね。
Guidanceにやるべきことが記載されているので、その通りに進めてみます。

該当ファイルを確認すると以下のようにコメントでもサポートが記載されています。

<?php

namespace App\Services;

use App\Exceptions\DuplicateTaskException;
use App\Models\Task;
use App\Repositories\Task\TaskRepositoryInterface;

class TaskService
{
    public function __construct(
        private readonly TaskRepositoryInterface $taskRepository,
    ) {}

    public function create(array $data): Task
    {
        // TODO(human): ここにビジネスロジックを実装してください
        // ヒント: 同一ユーザーの同名タスク重複チェック → DuplicateTaskException をスロー
        // その後、$this->taskRepository->create($data) でタスクを作成して返す
    }
}

以下のように実装してみました。

public function create(array $data): Task
{
    // TODO(human): ここにビジネスロジックを実装してください
    $isDuplicate = $this->taskRepository->existsByTitleForUser($data['title'], $data['userId']);
    // ヒント: 同一ユーザーの同名タスク重複チェック → DuplicateTaskException をスロー
    if($isDuplicate) {
        throw new DuplicateTaskException('同じタイトルのタスクがすでに存在しています。');
    }
    // その後、$this->taskRepository->create($data) でタスクを作成して返す
    return $this->taskRepository->create($data);
}

Guidanceに「重複チェックをServiceに置く理由を意識してみてください(FormRequestではなくServiceにある理由は何でしょう?) 」とコードを書く以外の問題も出してくれていますね。
自分なりに理由を考えた上で後で答え合わせをしてくれる感じかな。。。?

実装が終わればコマンドに「実装が完了した」と入力してやればclaudeは作業を再開してくれます。

⏺ 実装を確認しました。ロジックの構造は正しいです。一点だけ修正が必要です:$data['userId'] → $data['user_id'](Laravelの規約ではスネークケース)。

⏺ Update(app/Services/TaskService.php)
  ⎿  Added 3 lines, removed 5 lines
      14
      15      public function create(array $data): Task
      16      {
      17 -        // TODO(human): ここにビジネスロジックを実装してください
      18 -        $isDuplicate = $this->taskRepository->existsByTitleForUser($data['title'], $data['userId']);
      19 -        // ヒント: 同一ユーザーの同名タスク重複チェック → DuplicateTaskException をスロー
      20 -        if($isDuplicate) {
      17 +        $isDuplicate = $this->taskRepository->existsByTitleForUser($data['title'], $data['user_id']);
      18 +        if ($isDuplicate) {
      19              throw new DuplicateTaskException('同じタイトルのタスクがすでに存在しています。');
      20          }
      23 -        // その後、$this->taskRepository->create($data) でタスクを作成して返す
      21 +
      22          return $this->taskRepository->create($data);
      23      }
      24  }

⏺ 重複チェックをService層に置いたのは正しい判断です。FormRequestはHTTP入力の「形式」バリデーション(型・必須・形式)担当で、「同一ユーザーに同名タスクが存在するか」というビジネスルールはドメインの知識です。このルールはバッチ処理やコンソールコマンドからも
  タスク作成が呼ばれる可能性があり、その場合FormRequestを経由しないためServiceに置くことで常に保護されます。

  残りを実装します。

⏺ Bash(mkdir -p {プロジェクト名}/app/Http/Controllers/Api)
  ⎿  Done

⏺ Write(app/Http/Controllers/Api/TaskController.php)
  ⎿  Wrote 24 lines to app/Http/Controllers/Api/TaskController.php
      1 <?php
      2
      3 namespace App\Http\Controllers\Api;
      4
      5 use App\Http\Controllers\Controller;
      6 use App\Http\Requests\Task\StoreTaskRequest;
      7 use App\Http\Resources\Task\TaskResource;
      8 use App\Services\TaskService;
      9 use Illuminate\Http\JsonResponse;
     … +14 lines (ctrl+o to expand)

⏺ Write(app/Providers/AppServiceProvider.php)
  ⎿  Added 8 lines, removed 13 lines
       2
       3  namespace App\Providers;
       4
       5 +use App\Repositories\Task\TaskRepository;
       6 +use App\Repositories\Task\TaskRepositoryInterface;
       7  use Illuminate\Support\ServiceProvider;
       8
       9  class AppServiceProvider extends ServiceProvider
      10  {
       9 -    /**
      10 -     * Register any application services.
      11 -     *
      12 -     * @return void
      13 -     */
      14 -    public function register()
      11 +    public function register(): void
      12      {
      16 -        //
      13 +        $this->app->bind(
      14 +            TaskRepositoryInterface::class,
      15 +            TaskRepository::class,
      16 +        );
      17      }
      18
      19 -    /**
      20 -     * Bootstrap any application services.
      21 -     *
      22 -     * @return void
      23 -     */
      24 -    public function boot()
      19 +    public function boot(): void
      20      {
      21          //
      22      }

⏺ 次に、DuplicateTaskException が発生したときに適切な422レスポンスを返すよう Handler.php を更新し、ルートを追加します。


⏺ Update(app/Exceptions/Handler.php)
  ⎿  Added 1 line
      3  namespace App\Exceptions;
      4
      5  use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
      6 +use Illuminate\Http\JsonResponse;
      7  use Throwable;
      8
      9  class Handler extends ExceptionHandler

⏺ Update(app/Exceptions/Handler.php)
  ⎿  Added 4 lines
      38          $this->reportable(function (Throwable $e) {
      39              //
      40          });
      41 +
      42 +        $this->renderable(function (DuplicateTaskException $e) {
      43 +            return new JsonResponse(['message' => $e->getMessage()], 422);
      44 +        });
      45      }
      46  }

⏺ Update(routes/api.php)
  ⎿  File must be read first


⏺ Update(routes/api.php)
  ⎿  Added 4 lines
      17  Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
      18      return $request->user();
      19  });
      20 +
      21 +Route::middleware('auth:sanctum')->group(function () {
      22 +    Route::post('/tasks', [\App\Http\Controllers\Api\TaskController::class, 'store']);
      23 +});

⏺ 実装完了です。作成したファイルの一覧:

  app/
  ├── Models/Task.php
  ├── Repositories/Task/
  │   ├── TaskRepositoryInterface.php
  │   └── TaskRepository.php
  ├── Http/
  │   ├── Requests/Task/StoreTaskRequest.php
  │   ├── Resources/Task/TaskResource.php
  │   └── Controllers/Api/TaskController.php
  ├── Services/TaskService.php
  ├── Exceptions/DuplicateTaskException.php
  └── Providers/AppServiceProvider.php  (更新)
  routes/api.php  (更新)

  エンドポイント: POST /api/tasks (要: Sanctum認証)

  ★ Insight ─────────────────────────────────────
  Handler.php の renderable() に例外→HTTPレスポンスの変換を集約することで、「誰がキャッチするか」を考えずに例外をスローできます。DuplicateTaskException は「422 Unprocessable Entity」が適切で、「404 Not
  Found」や「500」ではなくドメインルール違反を表す422を使うことでAPIクライアントが状況を正確に判断できます。
  ─────────────────────────────────────────────────

TaskService以外の実装は全てclaudeが実行してくれましたね。

重複チェックをService層に置いたのは正しい判断です。FormRequestはHTTP入力の「形式」バリデーション(型・必須・形式)担当で、「同一ユーザーに同名タスクが存在するか」というビジネスルールはドメインの知識です。このルールはバッチ処理やコンソールコマンドからも
  タスク作成が呼ばれる可能性があり、その場合FormRequestを経由しないためServiceに置くことで常に保護されます。

出していた問題に対しても答えをしっかり書いてくれています。
答えを読むとなるほど!とこっちも勉強になる内容でした笑

実装を確認しました。ロジックの構造は正しいです。一点だけ修正が必要です:$data['userId'] → $data['user_id'](Laravelの規約ではスネークケース)。

ちなみに先ほど書いたコードに誤りがあって指摘を受けましたね笑
書いたコードが間違っていれば指摘をした上で修正もclaude側で実行してくれます。

最後に

思ったよりもLearningモードが実用的な問題を出してくれて面白いと思いました。
解説は多少自分でも調べないと理解しきれない内容となっていそうなので、
調べる力とかも一緒に鍛えてくれそうです。

各モードの所感まとめ

Defaultモード: 爆速でコードを書き上げたい実務向け。

Explanatoryモード: 新しい現場に入ったジュニア〜シニア層向け。コードの意図やプロジェクトの規約(CLAUDE.md)を解説してくれるため、キャッチアップに最適。

Learningモード: ペアプロ・オンボーディング向け。ビジネスロジックの根幹など「人間が考えるべき箇所」をあえてTODOにして問いかけてくるため、初学者の育成に非常に有効。

興味を持った方はぜひ触ってみてください。

補足

事前に定義したCLAUDE.mdは以下の定義をしていました。

CLAUDE.md
# CLAUDE.md — タスク管理ツール API

## プロジェクト概要

Laravel 8 を使用したタスク管理ツールの REST API。

---

## 技術スタック

- **PHP**: 8.0+
- **Framework**: Laravel 8.x
- **DB**: MySQL 8.0
- **認証**: Laravel Sanctum

---

## アーキテクチャ原則

### 層の責務

Request
  └─ FormRequest        # バリデーション
      └─ Controller     # ルーティング・レスポンス整形のみ
          └─ Service    # ビジネスロジック
              ├─ Usecase    # 複数Serviceまたは処理をまたぐ共通ロジック
              └─ Repository # データアクセス(Eloquent)
                  └─ Resource   # レスポンス整形


### 各層のルール

#### Controller
- ビジネスロジックを **書かない**
- `FormRequest` でバリデーション済みのデータを受け取り、`Service` or `Usecase` に委譲する
- レスポンスは必ず `Resource` クラスを使用する
- 例外ハンドリングは `Handler.php` に集約し、Controller には書かない


// Good
public function store(StoreTaskRequest $request): JsonResponse
{
    $task = $this->taskService->create($request->validated());
    return new JsonResponse(new TaskResource($task), 201);
}

// Bad — Controllerにロジックを書かない
public function store(Request $request): JsonResponse
{
    $task = Task::create([...]);
    return response()->json($task);
}



#### FormRequest(バリデーション層)
- バリデーションルールはすべてカスタム `FormRequest` クラスに定義する
- `Controller` 内での `$request->validate()` は **禁止**
- `authorize()` メソッドでポリシーの呼び出しも行う
- クラス名は `{動詞}{リソース名}Request` とする(例: `StoreTaskRequest`)


// app/Http/Requests/Task/StoreTaskRequest.php
class StoreTaskRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true; // またはPolicyを呼び出す
    }

    public function rules(): array
    {
        return [
            'title'       => ['required', 'string', 'max:255'],
            'description' => ['nullable', 'string'],
            'due_date'    => ['nullable', 'date', 'after_or_equal:today'],
            'status'      => ['required', Rule::in(['todo', 'in_progress', 'done'])],
            'priority'    => ['required', Rule::in(['low', 'medium', 'high'])],
        ];
    }
}


#### Service層(ビジネスロジック)
- ビジネスルール・状態遷移・トランザクション管理を担う
- Eloquent を **直接使わず**、必ず `Repository` 経由でデータにアクセスする
- 1クラス1リソースを原則とする(例: `TaskService`)
- クラス名は `{リソース名}Service` とする


// app/Services/TaskService.php
class TaskService
{
    public function __construct(
        private readonly TaskRepositoryInterface $taskRepository,
    ) {}

    public function create(array $data): Task
    {
        // ビジネスルール: 同一ユーザーの同名タスクは作成不可
        if ($this->taskRepository->existsByTitleForUser($data['title'], $data['user_id'])) {
            throw new DuplicateTaskException('同名のタスクが既に存在します。');
        }

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

    public function updateStatus(int $taskId, string $newStatus): Task
    {
        $task = $this->taskRepository->findOrFail($taskId);

        // 状態遷移バリデーション
        TaskStatusTransition::validate($task->status, $newStatus);

        return $this->taskRepository->update($task, ['status' => $newStatus]);
    }
}


#### Usecase層(共通ロジック)
- 複数の `Service` をまたぐ処理や、再利用される共通フローを担う
- 単一の `Service` しか使わない処理は `Service` に置き、`Usecase` は作らない
- クラス名は処理内容を動詞+名詞で表す(例: `CompleteTaskUsecase`)
- `__invoke()` を実装し、単一責任を徹底する


// app/Usecases/Task/CompleteTaskUsecase.php
class CompleteTaskUsecase
{
    public function __construct(
        private readonly TaskService $taskService,
        private readonly NotificationService $notificationService,
    ) {}

    public function __invoke(int $taskId, int $userId): Task
    {
        // タスク完了 + 通知送信をアトミックに扱う
        return DB::transaction(function () use ($taskId, $userId) {
            $task = $this->taskService->updateStatus($taskId, 'done');
            $this->notificationService->notifyCompletion($userId, $task);
            return $task;
        });
    }
}


#### Repository層(データアクセス層)
- Eloquent の操作はすべてここに閉じ込める
- 必ずインターフェースを定義し、`AppServiceProvider` でバインドする
- クラス名は `{リソース名}Repository` / インターフェースは `{リソース名}RepositoryInterface`
- `Service` から直接 `Model::find()` などを呼ぶのは **禁止**


// app/Repositories/Task/TaskRepositoryInterface.php
interface TaskRepositoryInterface
{
    public function findOrFail(int $id): Task;
    public function findAllByUser(int $userId, array $filters): Collection;
    public function create(array $data): Task;
    public function update(Task $task, array $data): Task;
    public function delete(Task $task): void;
    public function existsByTitleForUser(string $title, int $userId): bool;
}

// app/Repositories/Task/TaskRepository.php
class TaskRepository implements TaskRepositoryInterface
{
    public function findOrFail(int $id): Task
    {
        return Task::findOrFail($id);
    }

    public function findAllByUser(int $userId, array $filters): Collection
    {
        return Task::query()
            ->where('user_id', $userId)
            ->when(isset($filters['status']), fn($q) => $q->where('status', $filters['status']))
            ->when(isset($filters['priority']), fn($q) => $q->where('priority', $filters['priority']))
            ->orderBy('due_date')
            ->get();
    }

    public function create(array $data): Task
    {
        return Task::create($data);
    }

    public function update(Task $task, array $data): Task
    {
        $task->update($data);
        return $task->fresh();
    }

    public function delete(Task $task): void
    {
        $task->delete();
    }

    public function existsByTitleForUser(string $title, int $userId): bool
    {
        return Task::where('user_id', $userId)->where('title', $title)->exists();
    }
}


#### Resource層(レスポンス整形)
- APIレスポンスのJSONフォーマットはすべて `Resource` クラスで定義する
- `response()->json($model)` や `$model->toArray()` の直接返却は **禁止**
- 一覧は `ResourceCollection`、単体は `Resource` を使い分ける
- クラス名は `{リソース名}Resource` / `{リソース名}Collection`


// app/Http/Resources/Task/TaskResource.php
class TaskResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id'          => $this->id,
            'title'       => $this->title,
            'description' => $this->description,
            'status'      => $this->status,
            'priority'    => $this->priority,
            'due_date'    => $this->due_date?->toDateString(),
            'created_at'  => $this->created_at->toIso8601String(),
            'updated_at'  => $this->updated_at->toIso8601String(),
        ];
    }
}

// app/Http/Resources/Task/TaskCollection.php
class TaskCollection extends ResourceCollection
{
    public $collects = TaskResource::class;

    public function toArray($request): array
    {
        return [
            'data' => $this->collection,
            'meta' => [
                'total' => $this->collection->count(),
            ],
        ];
    }
}


---

## ディレクトリ構成


app/
├── Http/
│   ├── Controllers/
│   │   └── Api/
│   │       ├── TaskController.php
│   │       ├── UserController.php
│   │       └── AuthController.php
│   ├── Requests/
│   │   ├── Task/
│   │   │   ├── StoreTaskRequest.php
│   │   │   ├── UpdateTaskRequest.php
│   │   │   └── CompleteTaskRequest.php
│   │   ├── User/
│   │   │   ├── StoreUserRequest.php
│   │   │   └── UpdateUserRequest.php
│   │   └── Auth/
│   │       └── LoginRequest.php
│   └── Resources/
│       ├── Task/
│       │   ├── TaskResource.php
│       │   └── TaskCollection.php
│       └── User/
│           └── UserResource.php
├── Services/
│   ├── TaskService.php
│   ├── UserService.php
│   └── AuthService.php
├── Usecases/
│   └── Task/
│       └── CompleteTaskUsecase.php
├── Repositories/
│   ├── Task/
│   │   ├── TaskRepositoryInterface.php
│   │   └── TaskRepository.php
│   └── User/
│       ├── UserRepositoryInterface.php
│       └── UserRepository.php
└── Models/
    ├── Task.php
    └── User.php


---

## APIエンドポイント設計

| Method | URI | Action | FormRequest |
|--------|-----|--------|-------------|
| GET | `/api/tasks` | 一覧取得 | `IndexTaskRequest` |
| POST | `/api/tasks` | 作成 | `StoreTaskRequest` |
| GET | `/api/tasks/{id}` | 詳細取得 | — |
| PUT | `/api/tasks/{id}` | 更新 | `UpdateTaskRequest` |
| DELETE | `/api/tasks/{id}` | 削除 | — |
| PATCH | `/api/tasks/{id}/complete` | 完了 | `CompleteTaskRequest` |

---

## コーディング規約

### 命名規則

| 種別 | 規則 | 例 |
|------|------|----|
| Controller | `{リソース}Controller` | `TaskController` |
| FormRequest | `{動詞}{リソース}Request` | `StoreTaskRequest` |
| Service | `{リソース}Service` | `TaskService` |
| Usecase | `{動詞}{目的語}Usecase` | `CompleteTaskUsecase` |
| Repository | `{リソース}Repository` | `TaskRepository` |
| Interface | `{リソース}RepositoryInterface` | `TaskRepositoryInterface` |
| Resource | `{リソース}Resource` | `TaskResource` |
| Collection | `{リソース}Collection` | `TaskCollection` |

### 禁止事項

- [ ] Controller 内でのビジネスロジック記述
- [ ] Controller 内での `$request->validate()` 使用
- [ ] Service / Usecase 内での Eloquent 直接操作
- [ ] Controller でのモデル直接返却(`response()->json($model)`)
- [ ] Repository インターフェースを介さない直接インスタンス化

### DI(依存性注入)
- コンストラクタインジェクションを使用する
- `readonly` プロパティで不変性を保証する
- Repository は必ずインターフェース経由で注入する


public function __construct(
    private readonly TaskRepositoryInterface $taskRepository,
) {}


### トランザクション
- 複数テーブルへの書き込みを伴う処理は必ず `DB::transaction()` で囲む
- トランザクション管理は `Usecase` または `Service` 層に置く

### 例外処理
- ドメイン固有の例外は `app/Exceptions/` に定義する
- `Handler.php` でまとめてHTTPレスポンスに変換する
- Controller 内での `try-catch` は原則禁止

---

## Service Provider バインド設定


// app/Providers/AppServiceProvider.php
public function register(): void
{
    $this->app->bind(
        TaskRepositoryInterface::class,
        TaskRepository::class,
    );
}


---

## テスト方針

- **Feature Test**: エンドポイント単位でリクエスト〜レスポンスを検証
- **Unit Test**: `Service` / `Usecase` のビジネスロジックを単体で検証
- Repository は `mock` でテストし、DBへの依存を排除する
- テストファイルは `tests/Feature/Api/` と `tests/Unit/Services/` に配置する
株式会社アクトビ

Discussion