Laravel11でAPIの実装する

## 概要
この記事ではLaravel11を利用して基本的なAPIを実装手順を紹介します。

## 実装する機能
- 商品一覧の取得（検索、ソート、ページネーション）
- 商品の登録
- 商品の詳細取得
- 商品の更新
- 商品の削除（論理削除、物理削除、復元）

## 実装手順

### 1. Laravel11のプロジェクトの作成

```bash
composer create-project laravel/laravel=11.x product-management-api
cd product-management-api
```

### 2. モデルとマイグレーションの作成

まず、Product モデルとマイグレーションファイルを作成します。

```bash
php artisan make:model Product -m
```

マイグレーションファイル（`database/migrations/xxxx_xx_xx_create_products_table.php`）:

```php
public function up(): void
{
    Schema::create('products', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->text('description')->nullable();
        $table->integer('price');
        $table->integer('stock');
        $table->boolean('is_active')->default(true);
        $table->timestamps();
        $table->softDeletes();
    });
}
```

Product モデル（`app/Models/Product.php`）:

```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Product extends Model
{
    use HasFactory, SoftDeletes;

    protected $fillable = [
        'name',
        'description',
        'price',
        'stock',
        'is_active'
    ];

    protected $casts = [
        'price' => 'integer',
        'stock' => 'integer',
        'is_active' => 'boolean',
    ];
}
```

### 3. API リソースの作成

```bash
php artisan make:resource ProductResource
```

`app/Http/Resources/ProductResource.php`:

```php
<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class ProductResource extends JsonResource
{

    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'description' => $this->description,
            'price' => $this->price,
            'stock' => $this->stock,
            'is_active' => $this->is_active,
            'created_at' => $this->created_at->format('Y-m-d H:i:s'),
            'updated_at' => $this->updated_at->format('Y-m-d H:i:s'),
        ];
    }
}

```

### 4. バリデーションの作成

商品登録用のリクエストクラス:

```bash
php artisan make:request Api/CreateProductRequest
```

```php
<?php

namespace App\Http\Requests\Api;

use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use Symfony\Component\HttpFoundation\Response;

class CreateProductRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'description' => ['nullable', 'string', 'max:1000'],
            'price' => ['required', 'integer', 'min:0'],
            'stock' => ['required', 'integer', 'min:0'],
            'is_active' => ['boolean'],
        ];
    }

    public function messages(): array
    {
        return [
            'name.required' => '商品名は必須です',
            'name.max' => '商品名は255文字以内で入力してください',
            'description.max' => '商品説明は1000文字以内で入力してください',
            'price.required' => '価格は必須です',
            'price.integer' => '価格は整数で入力してください',
            'price.min' => '価格は0以上で入力してください',
            'stock.required' => '在庫数は必須です',
            'stock.integer' => '在庫数は整数で入力してください',
            'stock.min' => '在庫数は0以上で入力してください',
            'is_active.boolean' => '商品状態は真偽値で入力してください',
        ];
    }

    protected function failedValidation(Validator $validator)
    {
        throw new HttpResponseException(
            response()->json([
                'message' => 'Validation failed',
                'errors' => $validator->errors(),
            ], Response::HTTP_UNPROCESSABLE_ENTITY)
        );
    }
}
```

商品更新用のリクエストクラス:

```bash
php artisan make:request Api/UpdateProductRequest
```

```php
<?php

namespace App\Http\Requests\Api;

class UpdateProductRequest extends CreateProductRequest
{
    public function rules(): array
    {
        $rules = parent::rules();
        
        // 更新時は送信されたフィールドのみをバリデーション
        return array_map(function ($rule) {
            return array_merge(['sometimes'], $rule);
        }, $rules);
    }
}
```

### 5. コントローラーの作成

```bash
php artisan make:controller Api/ProductController
```

`app/Http/Controllers/Api/ProductController.php`:

```php
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\Api\CreateProductRequest;
use App\Http\Requests\Api\UpdateProductRequest;
use App\Http\Resources\ProductResource;
use App\Models\Product;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Symfony\Component\HttpFoundation\Response;

class ProductController extends Controller
{
    /**
     * 許可されているソートカラムのリスト
     */
    private const ALLOWED_SORT_COLUMNS = [
        'id',
        'name',
        'price',
        'stock',
        'created_at',
        'updated_at'
    ];

    /**
     * 商品一覧を取得
     */
    public function index(Request $request): AnonymousResourceCollection
    {
        try {
            $query = Product::query();

            // 検索条件の追加
            if ($request->has('name')) {
                $query->where('name', 'like', '%' . $request->input('name') . '%');
            }

            if ($request->has('is_active')) {
                $query->where('is_active', $request->boolean('is_active'));
            }

            // ソート条件の追加
            $sortBy = $request->input('sort_by', 'created_at');
            $sortOrder = $request->input('sort_order', 'desc');

            // 不正なソートカラムのチェック
            if (!in_array($sortBy, self::ALLOWED_SORT_COLUMNS, true)) {
                throw new \InvalidArgumentException("Invalid sort column: {$sortBy}");
            }

            $query->orderBy($sortBy, $sortOrder);

            // ページネーション
            $products = $query->paginate($request->input('per_page', 15));

            return ProductResource::collection($products);
        } catch (\InvalidArgumentException $e) {
            abort(Response::HTTP_BAD_REQUEST, $e->getMessage());
        } catch (\Exception $e) {
            abort(Response::HTTP_INTERNAL_SERVER_ERROR, 'Failed to fetch products.');
        }
    }

    /**
     * 商品を作成
     */
    public function store(CreateProductRequest $request): JsonResponse
    {
        try {
            $product = Product::create($request->validated());

            return response()->json([
                'message' => 'Product created successfully',
                'data' => new ProductResource($product)
            ], Response::HTTP_CREATED);
        } catch (\Exception $e) {
            return response()->json([
                'message' => 'Failed to create product',
                'error' => $e->getMessage()
            ], Response::HTTP_INTERNAL_SERVER_ERROR);
        }
    }

    /**
     * 商品を更新
     */
    public function update(UpdateProductRequest $request, int $id): JsonResponse
    {
        try {
            $product = Product::findOrFail($id);

            $product->update($request->validated());

            return response()->json([
                'message' => 'Product updated successfully',
                'data' => new ProductResource($product)
            ], Response::HTTP_OK);
        } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
            return response()->json([
                'message' => 'Product not found'
            ], Response::HTTP_NOT_FOUND);
        } catch (\Exception $e) {
            return response()->json([
                'message' => 'Failed to update product',
                'error' => $e->getMessage()
            ], Response::HTTP_INTERNAL_SERVER_ERROR);
        }
    }

    /**
     * 商品を取得
     */
    public function show(int $id): JsonResponse
    {
        try {
            $product = Product::findOrFail($id);

            return response()->json([
                'data' => new ProductResource($product)
            ], Response::HTTP_OK);
        } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
            return response()->json([
                'message' => 'Product not found'
            ], Response::HTTP_NOT_FOUND);
        } catch (\Exception $e) {
            return response()->json([
                'message' => 'Failed to fetch product',
                'error' => $e->getMessage()
            ], Response::HTTP_INTERNAL_SERVER_ERROR);
        }
    }

    /**
     * 商品を削除
     */
    public function destroy(int $id): JsonResponse
    {
        try {
            // withTrashedを追加して、削除済みのレコードも検索対象に含める
            $product = Product::withTrashed()->findOrFail($id);

            if ($product->trashed()) {
                return response()->json([
                    'message' => 'Product is already deleted'
                ], Response::HTTP_BAD_REQUEST);
            }

            $product->delete();

            return response()->json([
                'message' => 'Product deleted successfully'
            ], Response::HTTP_OK);
        } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
            return response()->json([
                'message' => 'Product not found'
            ], Response::HTTP_NOT_FOUND);
        } catch (\Exception $e) {
            return response()->json([
                'message' => 'Failed to delete product',
                'error' => $e->getMessage()
            ], Response::HTTP_INTERNAL_SERVER_ERROR);
        }
    }

    /**
     * 商品を強制的に削除
     */
    public function forceDestroy(int $id): JsonResponse
    {
        try {
            $product = Product::withTrashed()->findOrFail($id);

            $product->forceDelete();

            return response()->json([
                'message' => 'Product permanently deleted successfully'
            ], Response::HTTP_OK);
        } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
            return response()->json([
                'message' => 'Product not found'
            ], Response::HTTP_NOT_FOUND);
        } catch (\Exception $e) {
            return response()->json([
                'message' => 'Failed to permanently delete product',
                'error' => $e->getMessage()
            ], Response::HTTP_INTERNAL_SERVER_ERROR);
        }
    }

    /**
     * 削除済み商品の復元
     */
    public function restore(int $id): JsonResponse
    {
        try {
            $product = Product::withTrashed()->findOrFail($id);

            if (!$product->trashed()) {
                return response()->json([
                    'message' => 'Product is not deleted'
                ], Response::HTTP_BAD_REQUEST);
            }

            $product->restore();

            return response()->json([
                'message' => 'Product restored successfully',
                'data' => new ProductResource($product)
            ], Response::HTTP_OK);
        } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
            return response()->json([
                'message' => 'Product not found'
            ], Response::HTTP_NOT_FOUND);
        } catch (\Exception $e) {
            return response()->json([
                'message' => 'Failed to restore product',
                'error' => $e->getMessage()
            ], Response::HTTP_INTERNAL_SERVER_ERROR);
        }
    }
}

```

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

Laravel11のデフォルトでは`routes/api.php`が存在しないため以下のコマンドで生成してください。

```bash
php artisan install:api
```

`routes/api.php`:

```php
<?php

use App\Http\Controllers\Api\ProductController;

Route::get('/products', [ProductController::class, 'index']);
Route::post('/products', [ProductController::class, 'store']);
Route::get('/products/{id}', [ProductController::class, 'show']);
Route::put('/products/{id}', [ProductController::class, 'update']);
Route::delete('/products/{id}', [ProductController::class, 'destroy']);
Route::delete('/products/{id}/force', [ProductController::class, 'forceDestroy']);
Route::patch('/products/{id}/restore', [ProductController::class, 'restore']);
```

### 6. テストの作成

```bash
php artisan make:test Api/ProductTest
```

`tests/Feature/Api/ProductTest.php`:
```php
<?php

namespace Tests\Feature\Api;

use App\Models\Product;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Symfony\Component\HttpFoundation\Response;
use Tests\TestCase;

/**
 * 商品APIのテスト
 * 
 */
class ProductTest extends TestCase
{
    use RefreshDatabase;

    /**
     * テストデータの作成
     */
    public function setUp(): void
    {
        parent::setUp();
        // テストデータの作成
        Product::factory(30)->create();
    }

    /**
     * 商品一覧を取得できるかのテスト
     */
    public function test_can_fetch_products_list(): void
    {
        $response = $this->getJson('/api/products');

        $response->assertStatus(200)
            ->assertJsonStructure([
                'data' => [
                    '*' => [
                        'id',
                        'name',
                        'description',
                        'price',
                        'stock',
                        'is_active',
                        'created_at',
                        'updated_at',
                    ],
                ],
                'links',
                'meta',
            ]);
    }

    /**
     * 商品名で検索できるかのテスト
     */
    public function test_can_fetch_products_with_search_params(): void
    {
        // 特定の名前を持つ商品を作成
        $searchName = 'テスト商品';
        Product::factory()->create(['name' => $searchName]);

        $response = $this->getJson("/api/products?name={$searchName}");

        $response->assertStatus(200)
            ->assertJsonFragment(['name' => $searchName]);
    }

    /**
     * アクティブな商品のみ取得できるかのテスト
     */
    public function test_can_fetch_active_products_only(): void
    {
        // アクティブな商品を作成
        Product::factory(5)->create(['is_active' => true]);
        // 非アクティブな商品を作成
        Product::factory(5)->create(['is_active' => false]);

        $response = $this->getJson('/api/products?is_active=true');

        $response->assertStatus(200);
        $products = $response->json('data');
        collect($products)->each(function ($product) {
            $this->assertTrue($product['is_active']);
        });
    }

    /**
     * 商品を価格でソートできるかのテスト
     */
    public function test_can_sort_products(): void
    {
        // 価格の異なる商品を作成
        Product::factory()->create(['price' => 1000]);
        Product::factory()->create(['price' => 2000]);
        Product::factory()->create(['price' => 3000]);

        $response = $this->getJson('/api/products?sort_by=price&sort_order=desc');

        $response->assertStatus(200);
        $products = $response->json('data');
        $this->assertTrue($products[0]['price'] > $products[1]['price']);
    }

    /**
     * ページネーションができるかのテスト
     */
    public function test_can_paginate_products(): void
    {
        $perPage = 5;
        $response = $this->getJson("/api/products?per_page={$perPage}");

        $response->assertStatus(200)
            ->assertJsonCount($perPage, 'data')
            ->assertJsonStructure([
                'meta' => [
                    'current_page',
                    'from',
                    'last_page',
                    'per_page',
                    'to',
                    'total',
                ],
            ]);
    }

    /**
     * 無効なソートパラメータのテスト
     */
    public function test_handles_invalid_sort_parameter(): void
    {
        $response = $this->getJson('/api/products?sort_by=invalid_column');

        $response->assertStatus(Response::HTTP_BAD_REQUEST)
            ->assertJsonFragment([
                'message' => 'Invalid sort column: invalid_column'
            ]);
    }

    /**
     * 商品を作成できるかのテスト
     */
    public function test_can_create_product(): void
    {
        $productData = [
            'name' => 'テスト商品',
            'description' => '商品の説明文です',
            'price' => 1000,
            'stock' => 10,
            'is_active' => true,
        ];

        $response = $this->postJson('/api/products', $productData);

        $response->assertStatus(Response::HTTP_CREATED)
            ->assertJsonStructure([
                'message',
                'data' => [
                    'id',
                    'name',
                    'description',
                    'price',
                    'stock',
                    'is_active',
                    'created_at',
                    'updated_at',
                ]
            ]);

        $this->assertDatabaseHas('products', $productData);
    }

    /**
     * 無効なデータで商品を作成できないかのテスト
     */
    public function test_cannot_create_product_with_invalid_data(): void
    {
        $invalidData = [
            'name' => '', // 必須項目を空にする
            'price' => -1, // 負の値は不許可
            'stock' => 'invalid', // 数値以外は不許可
        ];

        $response = $this->postJson('/api/products', $invalidData);

        $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)
            ->assertJsonStructure([
                'message',
                'errors' => [
                    'name',
                    'price',
                    'stock',
                ]
            ]);
    }

    /**
     * 商品を更新できるかのテスト
     */
    public function test_can_update_product(): void
    {
        $product = Product::factory()->create();

        $updateData = [
            'name' => '更新後の商品名',
            'price' => 2000,
        ];

        $response = $this->putJson("/api/products/{$product->id}", $updateData);

        $response->assertStatus(Response::HTTP_OK)
            ->assertJsonStructure([
                'message',
                'data' => [
                    'id',
                    'name',
                    'description',
                    'price',
                    'stock',
                    'is_active',
                    'created_at',
                    'updated_at',
                ]
            ])
            ->assertJsonFragment([
                'name' => '更新後の商品名',
                'price' => 2000,
            ]);

        $this->assertDatabaseHas('products', $updateData);
    }

    /**
     * 無効なデータで商品を更新できないかのテスト
     */
    public function test_cannot_update_product_with_invalid_data(): void
    {
        $product = Product::factory()->create();

        $invalidData = [
            'name' => '', // 空の名前は不許可
            'price' => -1, // 負の価格は不許可
        ];

        $response = $this->putJson("/api/products/{$product->id}", $invalidData);

        $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)
            ->assertJsonStructure([
                'message',
                'errors' => [
                    'name',
                    'price',
                ]
            ]);
    }

    /**
     * 存在しない商品を更新しようとした場合のテスト
     */
    public function test_cannot_update_non_existent_product(): void
    {
        $nonExistentId = 9999;

        $updateData = [
            'name' => '更新後の商品名',
            'price' => 2000,
        ];

        $response = $this->putJson("/api/products/{$nonExistentId}", $updateData);

        $response->assertStatus(Response::HTTP_NOT_FOUND)
            ->assertJsonFragment([
                'message' => 'Product not found'
            ]);
    }

    /**
     * 商品を取得できるかのテスト
     */
    public function test_can_fetch_single_product(): void
    {
        $product = Product::factory()->create([
            'name' => 'テスト商品',
            'description' => '商品の説明文',
            'price' => 1000,
            'stock' => 10,
            'is_active' => true,
        ]);

        $response = $this->getJson("/api/products/{$product->id}");

        $response->assertStatus(Response::HTTP_OK)
            ->assertJsonStructure([
                'data' => [
                    'id',
                    'name',
                    'description',
                    'price',
                    'stock',
                    'is_active',
                    'created_at',
                    'updated_at',
                ]
            ])
            ->assertJsonFragment([
                'name' => 'テスト商品',
                'description' => '商品の説明文',
                'price' => 1000,
                'stock' => 10,
                'is_active' => true,
            ]);
    }

    /**
     * 存在しない商品を取得しようとした場合のテスト
     */
    public function test_cannot_fetch_non_existent_product(): void
    {
        $nonExistentId = 9999;

        $response = $this->getJson("/api/products/{$nonExistentId}");

        $response->assertStatus(Response::HTTP_NOT_FOUND)
            ->assertJsonFragment([
                'message' => 'Product not found'
            ]);
    }

    /**
     * 削除された商品を取得しようとした場合のテスト
     */
    public function test_cannot_fetch_deleted_product(): void
    {
        $product = Product::factory()->create();
        $product->delete();

        $response = $this->getJson("/api/products/{$product->id}");

        $response->assertStatus(Response::HTTP_NOT_FOUND)
            ->assertJsonFragment([
                'message' => 'Product not found'
            ]);
    }

    /**
     * 無効なID形式で商品を取得しようとした場合のテスト
     */
    public function test_returns_error_for_invalid_id_format(): void
    {
        $response = $this->getJson('/products/invalid-id');

        $response->assertStatus(Response::HTTP_NOT_FOUND);
    }

    /**
     * 商品を論理削除できるかのテスト
     */
    public function test_can_soft_delete_product(): void
    {
        $product = Product::factory()->create();

        $response = $this->deleteJson("/api/products/{$product->id}");

        $response->assertStatus(Response::HTTP_OK)
            ->assertJsonFragment([
                'message' => 'Product deleted successfully'
            ]);

        $this->assertSoftDeleted('products', [
            'id' => $product->id
        ]);
    }

    /**
     * 存在しない商品を削除しようとした場合のテスト
     */
    public function test_cannot_delete_non_existent_product(): void
    {
        $nonExistentId = 9999;

        $response = $this->deleteJson("/api/products/{$nonExistentId}");

        $response->assertStatus(Response::HTTP_NOT_FOUND)
            ->assertJsonFragment([
                'message' => 'Product not found'
            ]);
    }

    /**
     * 削除済みの商品を削除しようとした場合のテスト
     */
    public function test_cannot_delete_already_deleted_product(): void
    {
        $product = Product::factory()->create();
        $product->delete();

        $response = $this->deleteJson("/api/products/{$product->id}");

        $response->assertStatus(Response::HTTP_BAD_REQUEST)
            ->assertJsonFragment([
                'message' => 'Product is already deleted'
            ]);
    }

    /**
     * 商品を強制的に削除できるかのテスト
     */
    public function test_can_force_delete_product(): void
    {
        $product = Product::factory()->create();
        $product->delete(); // まずソフトデリート

        $response = $this->deleteJson("/api/products/{$product->id}/force");

        $response->assertStatus(Response::HTTP_OK)
            ->assertJsonFragment([
                'message' => 'Product permanently deleted successfully'
            ]);

        $this->assertDatabaseMissing('products', [
            'id' => $product->id
        ]);
    }

    /**
     * 削除済みの商品を復元できるかのテスト
     */
    public function test_can_restore_deleted_product(): void
    {
        $product = Product::factory()->create();
        $product->delete();

        $response = $this->patchJson("/api/products/{$product->id}/restore");  // postJsonからpatchJsonに変更

        $response->assertStatus(Response::HTTP_OK)
            ->assertJsonFragment([
                'message' => 'Product restored successfully'
            ]);

        $this->assertDatabaseHas('products', [
            'id' => $product->id,
            'deleted_at' => null
        ]);
    }

    /**
     * 削除済みでない商品を復元しようとした場合のテスト
     */
    public function test_cannot_restore_non_deleted_product(): void
    {
        $product = Product::factory()->create();

        $response = $this->patchJson("/api/products/{$product->id}/restore");  // postJsonからpatchJsonに変更

        $response->assertStatus(Response::HTTP_BAD_REQUEST)
            ->assertJsonFragment([
                'message' => 'Product is not deleted'
            ]);
    }
}
```
`.env`の内容をコピーして、`.env.testing`を作成する
```
 cp .env .env.testing
```
`.env.testing`に以下を追加:
```
DB_CONNECTION=sqlite
DB_DATABASE=:memory:
```


テスト実行:
```bash

php artisan test tests/Feature/Api/ProductTest.php

   PASS  Tests\Feature\Api\ProductTest
  ✓ can fetch products list                                                                                                                    0.14s  
  ✓ can fetch products with search params                                                                                                      0.04s  
  ✓ can fetch active products only                                                                                                             0.04s  
  ✓ can sort products                                                                                                                          0.03s  
  ✓ can paginate products                                                                                                                      0.04s  
  ✓ handles invalid sort parameter                                                                                                             0.03s  
  ✓ can create product                                                                                                                         0.04s  
  ✓ cannot create product with invalid data                                                                                                    0.04s  
  ✓ can update product                                                                                                                         0.03s  
  ✓ cannot update product with invalid data                                                                                                    0.03s  
  ✓ cannot update non existent product                                                                                                         0.04s  
  ✓ can fetch single product                                                                                                                   0.03s  
  ✓ cannot fetch non existent product                                                                                                          0.03s  
  ✓ cannot fetch deleted product                                                                                                               0.04s  
  ✓ returns error for invalid id format                                                                                                        0.03s  
  ✓ can soft delete product                                                                                                                    0.03s  
  ✓ cannot delete non existent product                                                                                                         0.04s  
  ✓ cannot delete already deleted product                                                                                                      0.03s  
  ✓ can force delete product                                                                                                                   0.03s  
  ✓ can restore deleted product                                                                                                                0.04s  
  ✓ cannot restore non deleted product                                                                                                         0.03s  

  Tests:    21 passed (230 assertions)
  Duration: 0.90s

```
### 7. ダミーデータの作成（オプション）
まず、ProductFactoryを作成します：
```bash
php artisan make:factory ProductFactory
```

`database/factories/ProductFactory.php`：

```php
<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class ProductFactory extends Factory
{
    public function definition(): array
    {
        return [
            'name' => fake()->realText(20),
            'description' => fake()->realText(200),
            'price' => fake()->numberBetween(100, 100000),
            'stock' => fake()->numberBetween(0, 100),
            'is_active' => fake()->boolean(80), // 80%の確率でtrue
            'created_at' => fake()->dateTimeBetween('-1 year', 'now'),
            'updated_at' => function (array $attributes) {
                return fake()->dateTimeBetween($attributes['created_at'], 'now');
            },
        ];
    }
}
```

次に、シーダーを作成します：
```bash
php artisan make:seeder ProductSeeder
```

`database/seeders/ProductSeeder.php`：

```php
<?php

namespace Database\Seeders;

use App\Models\Product;
use Illuminate\Database\Seeder;

class ProductSeeder extends Seeder
{
    public function run(): void
    {
        Product::factory(50)->create();
    }
}
```

`database/seeders/DatabaseSeeder.php`：

```php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        $this->call([
            ProductSeeder::class,
        ]);
    }
}
```

テストデータを投入します：
```bash
php artisan db:seed
```
## API エンドポイント一覧

| メソッド | エンドポイント | 説明 |
|---------|--------------|------|
| GET | /api/products | 商品一覧の取得 |
| POST | /api/products | 商品の登録 |
| GET | /api/products/{id} | 商品詳細の取得 |
| PUT | /api/products/{id} | 商品の更新 |
| DELETE | /api/products/{id} | 商品の論理削除 |
| DELETE | /api/products/{id}/force | 商品の物理削除 |
| PATCH | /api/products/{id}/restore | 削除商品の復元 |


## さいごに
この記事ではLaravel 11を使用して基本的な商品管理APIを実装しました。実装のポイントを振り返ってみます。

1. **基本に忠実な実装**
- Laravel の機能を活用したCRUD操作
- RESTful APIの原則に従った設計
- リクエストクラスによるバリデーション

2. **実践的な機能**
- 検索、ソート、ページネーション
- 論理削除と復元
- エラーハンドリング

3. **テストの重要性**
- 各機能の動作確認
- エッジケースのテスト
- テストコードによる仕様明確化

学んだ内容を土台として、認証機能の実装やキャッシュによるパフォーマンス改善、API仕様書の作成など、さらなる学習を進めていければと思います。