Contents
環境構築
1. ローカルマシンに PHP 8.3 と Composer をインストール
|
1 2 3 4 5 6 7 8 |
# macOS (Homebrew) の例 brew install php@8.3 composer # Ubuntu の場合 sudo apt-get update sudo apt-get install -y php8.3 php8.3-cli php8.3-mbstring php8.3-xml php8.3-bcmath unzip curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer |
ポイント
PHP 拡張は Laravel が要求するmbstring,openssl,pdo_mysqlなどを必ず入れておく。
2. Docker / Sail による開発環境
Laravel 11 は公式の Sail パッケージで Docker コンテナ化が推奨されています。以下は最小構成です。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
# プロジェクト作成(Sail 用オプション付き) composer create-project laravel/laravel todo-api "11.*" cd todo-api # Sail のインストール(開発依存)と起動スクリプト生成 composer require laravel/sail --dev php artisan sail:install --with=mysql,redis,mailhog # バックグラウンドでコンテナを立ち上げる ./vendor/bin/sail up -d |
コンテナ内の PHP‑FPM 起動コマンド(本番イメージ用)
Dockerfile に記述する CMD は次のように書くと、プロセスがフォアグラウンドで実行され、Docker のヘルスチェックが正しく働きます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# Dockerfile (php-fpm 用) FROM php:8.3-fpm-alpine # 必要な拡張をインストール RUN apk add --no-cache \ libpng-dev \ freetype-dev \ zip \ unzip && \ docker-php-ext-configure gd --with-freetype --with-png && \ docker-php-ext-install -j$(nproc) gd pdo_mysql bcmath # Composer のインストール COPY --from=composer:2 /usr/bin/composer /usr/bin/composer WORKDIR /var/www/html # アプリコードをコピー(キャッシュレイヤーの活用) COPY . . RUN composer install --no-dev --optimize-autoloader \ && php artisan config:cache \ && php artisan route:cache \ && php artisan view:cache EXPOSE 9000 CMD ["php-fpm", "-F"] # -F オプションでフォアグラウンド実行 |
docker-compose.yml の app サービスは上記イメージをビルドし、起動時にマイグレーションとキャッシュクリアを自動化します(後述の デプロイ 参照)。
.env 設定とデータベース接続
基本的な環境変数
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
APP_NAME=TodoAPI APP_ENV=local # 本番は production に変更 APP_KEY=base64:xxxxxxxx # `php artisan key:generate` で生成 APP_DEBUG=true # 本番は false 推奨 APP_URL=http://localhost LOG_CHANNEL=stack DB_CONNECTION=mysql DB_HOST=db # docker-compose のサービス名 DB_PORT=3306 DB_DATABASE=todo_api DB_USERNAME=sail DB_PASSWORD=password BROADCAST_DRIVER=log CACHE_DRIVER=file QUEUE_CONNECTION=redis SESSION_DRIVER=cookie MEMCACHED_HOST=127.0.0.1 SANCTUM_STATEFUL_DOMAINS=localhost,127.0.0.1 |
.env.exampleをリポジトリに残すことで、クローンした開発者はcp .env.example .env && php artisan key:generateだけで作業を開始できます。- 本番環境では
.env.productionを別ファイルとして管理し、CI/CD のデプロイ時に安全に上書きします。
プロジェクトのディレクトリ構成と命名規則
| ディレクトリ | 用途・ベストプラクティス |
|---|---|
app/Models |
Eloquent モデル(例: Todo.php) |
app/Http/Controllers/API |
API 専用コントローラ。Web 用は App\Http\Controllers\Web に分離 |
routes/api.php |
/api/* プレフィックス自動付与、認証ミドルウェアはここでまとめる |
database/migrations |
スキーマ定義ファイル。テーブル名は単数形ではなく複数形(todos) |
app/Http/Requests |
Form Request によるバリデーション・認可ロジック |
app/Http/Resources |
JSON 出力を統一する API Resource |
tests/Feature |
HTTP リクエストテスト(Pest / PHPUnit) |
docker/ |
Dockerfile、docker-compose.yml、CI 用設定ファイル等を格納 |
命名規則
- コントローラはTodoControllerのように単数形 +Controller。
- API Resource はTodoResource、コレクションは自動的にTodoCollectionになるが、::collection()を直接呼び出すのが一般的です。
Todo リソースの実装(モデル・マイグレーション)
|
1 2 |
php artisan make:model Todo -m |
マイグレーション (database/migrations/xxxx_xx_xx_create_todos_table.php)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { Schema::create('todos', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained()->cascadeOnDelete(); // 所有者 $table->string('title'); $table->text('description')->nullable(); $table->boolean('completed')->default(false); $table->timestamps(); }); } public function down(): void { Schema::dropIfExists('todos'); } }; |
モデル (app/Models/Todo.php)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Todo extends Model { use HasFactory; protected $fillable = ['title', 'description', 'completed']; // リレーション例(User が所有者) public function user() { return $this->belongsTo(User::class); } } |
マイグレーションは Docker 環境なら ./vendor/bin/sail artisan migrate、ローカルなら php artisan migrate で実行します。
RESTful ルーティング & API コントローラ
1. ルーティング
|
1 2 3 4 5 6 7 8 |
// routes/api.php use App\Http\Controllers\API\TodoController; use Illuminate\Support\Facades\Route; Route::middleware('auth:sanctum')->group(function () { Route::apiResource('todos', TodoController::class); }); |
apiResource が自動で index, store, show, update, destroy の 5 エンドポイントを生成します。
2. コントローラ作成
|
1 2 |
php artisan make:controller API/TodoController --api |
実装例(主要メソッド)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
namespace App\Http\Controllers\API; use App\Http\Controllers\Controller; use App\Http\Requests\StoreTodoRequest; use App\Http\Requests\UpdateTodoRequest; use App\Http\Resources\TodoResource; use App\Models\Todo; use Illuminate\Http\Response; class TodoController extends Controller { public function index() { $todos = Todo::with('user')->paginate(15); return TodoResource::collection($todos); } public function store(StoreTodoRequest $request) { /** @var \App\Models\User $user */ $user = $request->user(); $todo = $user->todos()->create($request->validated()); return (new TodoResource($todo)) ->response() ->setStatusCode(Response::HTTP_CREATED); } public function show(Todo $todo) { $this->authorize('view', $todo); // ポリシーで所有権チェック return new TodoResource($todo); } public function update(UpdateTodoRequest $request, Todo $todo) { $this->authorize('update', $todo); $todo->update($request->validated()); return new TodoResource($todo); } public function destroy(Todo $todo) { $this->authorize('delete', $todo); $todo->delete(); return response()->noContent(); } } |
- Eager Loading (
with('user')) で N+1 問題を回避。 - ポリシー による権限チェックは
php artisan make:policy TodoPolicyで作成し、AuthServiceProviderに登録します。
Form Request とバリデーション
作成コマンド
|
1 2 3 |
php artisan make:request StoreTodoRequest php artisan make:request UpdateTodoRequest |
StoreTodoRequest.php
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class StoreTodoRequest extends FormRequest { public function authorize(): bool { // 認可ロジックはポリシーに委譲することも可能 return $this->user()->can('create', Todo::class); } public function rules(): array { return [ 'title' => 'required|string|max:255', 'description' => 'nullable|string', 'completed' => 'boolean', ]; } } |
UpdateTodoRequest は同様ですが、sometimes ルールを付与すると部分更新が楽になります。
ベストプラクティス
- バリデーションは常にForm Requestに集約し、コントローラは「何をするか」だけを書く。
- カスタムバリデータやRule::uniqueのような複雑ロジックは Service クラス に切り出すとテストが容易です。
JSON 出力の統一(API Resource)
|
1 2 |
php artisan make:resource TodoResource |
TodoResource.php
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
namespace App\Http\Resources; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; class TodoResource extends JsonResource { public function toArray(Request $request): array { return [ 'id' => $this->id, 'title' => $this->title, 'description' => $this->description, 'completed' => (bool) $this->completed, 'owner' => [ 'id' => $this->user->id, 'name' => $this->user->name, ], 'created_at' => $this->created_at?->toIso8601String(), 'updated_at' => $this->updated_at?->toIso8601String(), ]; } } |
- 型キャスト (
(bool)) と ISO 8601 日付形式でフロントエンド側のパーサを楽にします。 TodoResource::collection($todos)が自動的にページネーション情報(meta,links)も付与します。
認証 – Laravel Sanctum の導入
インストールと設定
|
1 2 3 4 |
composer require laravel/sanctum php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider" php artisan migrate |
app/Http/Kernel.php(API グループ)
|
1 2 3 4 5 6 7 8 |
protected $middlewareGroups = [ 'api' => [ \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, 'throttle:api', \Illuminate\Routing\Middleware\SubstituteBindings::class, ], ]; |
トークン発行エンドポイント例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// routes/api.php use Illuminate\Support\Facades\Hash; use Illuminate\Http\Request; Route::post('login', function (Request $request) { $user = \App\Models\User::where('email', $request->email)->first(); if (! $user || ! Hash::check($request->password, $user->password)) { return response()->json(['message' => '認証に失敗しました'], 401); } $token = $user->createToken('api-token')->plainTextToken; return response()->json([ 'access_token' => $token, 'token_type' => 'Bearer', ]); }); |
- SPA の場合は
sanctum/csrf-cookieエンドポイントで CSRF トークンを取得し、Cookie 方式で認証できます。 - トークン失効 は
delete /api/logoutで$request->user()->tokens()->delete();とすれば完了。
テスト戦略(Postman・Pest・GitHub Actions)
1. Postman コレクション
| 手順 | 内容 |
|---|---|
| ① | 「New → Collection」→Todo API 作成。 |
| ② | 環境変数 BASE_URL に http://localhost(Sail)または本番 URL を設定。 |
| ③ | 各エンドポイントにリクエストを追加し、認証ヘッダー Authorization: Bearer {{access_token}} を設定。 |
| ④ | 「Tests」タブで簡易アサーションを書く(例:ステータス 200/201 の検証)。 |
| ⑤ | コレクションは JSON ファイルとしてエクスポートし、CI に組み込めるように newman で自動実行可能。 |
Newman 実行サンプル(GitHub Actions 用)
|
1 2 3 4 5 6 7 |
- name: Run Postman collection with Newman run: | npm install -g newman newman run collections/TodoAPI.postman_collection.json \ --environment environments/dev.postman_environment.json \ --env-var "BASE_URL=http://localhost" |
2. Pest(または PHPUnit)によるユニット/機能テスト
tests/Feature/TodoTest.php
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<?php use App\Models\User; use Laravel\Sanctum\Sanctum; it('認証済みユーザーは Todo を作成できる', function () { $user = User::factory()->create(); Sanctum::actingAs($user); $payload = [ 'title' => 'テストタスク', 'description' => 'Pest での自動テスト', ]; $response = $this->postJson('/api/todos', $payload); $response->assertCreated() ->assertJsonPath('data.title', 'テストタスク'); }); it('未認証ユーザーは Todo にアクセスできない', function () { $response = $this->getJson('/api/todos'); $response->assertUnauthorized(); }); |
php artisan testでローカル実行、CI ではvendor/bin/pest --ciを使用。- テストデータは Laravel Factories(
UserFactory,TodoFactory)を活用し、テーブルの状態管理はRefreshDatabaseトレイトで自動リセット。
3. GitHub Actions CI 設定
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
name: Laravel CI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest services: mysql: image: mysql:8 env: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: todo_api_test ports: ['3306:3306'] options: >- --health-cmd="mysqladmin ping -h127.0.0.1" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - uses: actions/checkout@v3 - name: Setup PHP 8.3 uses: shivammathur/setup-php@v2 with: php-version: '8.3' extensions: mbstring, intl, mysql, bcmath coverage: none - name: Install Composer dependencies run: composer install --prefer-dist --no-progress --no-suggest - name: Prepare environment file run: cp .env.example .env && php artisan key:generate - name: Run migrations env: DB_CONNECTION: mysql DB_HOST: 127.0.0.1 DB_PORT: 3306 DB_DATABASE: todo_api_test DB_USERNAME: root DB_PASSWORD: password run: php artisan migrate --force - name: Run Pest tests run: vendor/bin/pest --ci |
- キャッシュ(
actions/cache@v3)を追加すれば実行時間が大幅に短縮できます。 - 本番デプロイ時は同様の
docker-compose.ymlを利用し、GitHub Actions のpushイベントで自動ビルド・プッシュ (ghcr.io/yourorg/todo-api:latest) が可能です。
Docker 本番デプロイと php‑fpm 起動例
完全版 docker-compose.yml
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
version: '3.8' services: app: build: context: . dockerfile: Dockerfile image: ghcr.io/yourorg/todo-api:${TAG:-latest} env_file: - .env.production depends_on: - db ports: - "80:9000" command: > sh -c " php artisan migrate --force && php artisan config:cache && php artisan route:cache && php-fpm -F " db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} MYSQL_DATABASE: ${DB_DATABASE} MYSQL_USER: ${DB_USERNAME} MYSQL_PASSWORD: ${DB_PASSWORD} ports: - "3306:3306" volumes: - db-data:/var/lib/mysql redis: image: redis:7-alpine ports: - "6379:6379" volumes: db-data: |
デプロイ手順(CI/CD の一部として)
|
1 2 3 4 5 6 7 |
# CI が Docker イメージをビルドして GitHub Packages にプッシュした後のローカル実行例 docker compose pull app # 最新イメージ取得 docker compose up -d # バックグラウンドで起動 # ロギング・デバッグ docker logs -f todo-api_app_1 |
commandでは マイグレーション → キャッシュ生成 → php-fpm フォアグラウンド実行 の流れを一括化。php-fpm -F(--nodaemonize)により、Docker がプロセス終了を正しく検知できるので ヘルスチェック が機能します。
パフォーマンス・セキュリティ最適化ポイント
| 項目 | 内容 | 効果 |
|---|---|---|
キャッシュ (config:cache, route:cache) |
設定ファイルとルート情報を PHP の opcode キャッシュに変換。 | リクエストごとのディスク I/O を約 20‑30 % 削減 |
Eager Loading (with()) |
N+1 クエリ問題の根本解消。 | DB アクセス回数削減でレイテンシが大幅に改善 |
キュー (QUEUE_CONNECTION=redis) |
メール送信・通知処理を非同期化。 | HTTP スレッドがブロックしないため、平均応答時間が 30 % 程度向上 |
Rate Limiting (ThrottleRequests ミドルウェア) |
api グループにデフォルトで throttle:api が設定済み。 |
DoS 攻撃対策と API の安定運用 |
ヘッダー強化 (X-Content-Type-Options, Referrer-Policy) |
helmet に相当するミドルウェアは Laravel では SecureHeaders パッケージで実装可。 |
ブラウザ側の攻撃ベクトルを低減 |
ログ監視 (Log::channel('stack'), Sentry 等) |
エラーと例外を集中管理し、Slack 通知や外部 APM と連携。 | 障害検知が迅速になり、復旧時間が短縮 |
具体的なコード例
|
1 2 3 4 5 6 7 8 9 |
// App/Providers/AppServiceProvider.php public function boot() { // 全モデルで eager loading のデフォルト設定(必要に応じて調整) Todo::addGlobalScope('withUser', function (Builder $builder) { $builder->with('user'); }); } |
|
1 2 3 4 |
// Queue 設定例 (.env) QUEUE_CONNECTION=redis REDIS_HOST=redis |
|
1 2 3 4 5 |
// Rate limiting カスタマイズ(app/Providers/RouteServiceProvider.php) RateLimiter::for('api', function (Request $request) { return Limit::perMinute(120)->by(optional($request->user())->id ?: $request->ip()); }); |
まとめ
- Laravel 11 + PHP 8.3 の組み合わせは、モダンな API 開発に最適です。
- Docker / Sail による環境統一で「マシン依存」問題を回避し、
php-fpm -Fを用いた本番コンテナ起動が安全です。 - Form Request, API Resource, Sanctum といった Laravel 標準機能を活用すれば、認証・バリデーション・JSON 形状の統一がシンプルに実装できます。
- テスト は Postman → Newman、Pest → GitHub Actions の三層で自動化し、プッシュごとに品質保証を走らせましょう。
- パフォーマンス と セキュリティ はキャッシュ・Eager Loading・キュー・Rate Limiting などのベストプラクティスで補完します。
これらの手順を踏めば、開発から本番デプロイまで一貫したフローが確立でき、拡張性と保守性に優れた Todo API が完成します。
この記事は 2026 年 4 月時点の Laravel 11 と PHP 8.3 の最新情報を元に執筆しています。バージョンアップや公式パッケージの変更があった場合は、公式ドキュメントをご参照ください。